1use std::collections::{BTreeMap, BTreeSet};
4use std::error::Error;
5use std::fmt;
6use std::ops::Range;
7
8use super::{Grammar, GrammarExpr, GrammarRule};
9
10#[derive(Clone, Debug, PartialEq, Eq)]
12pub struct RuleSpan {
13 pub rule: String,
15 pub span: Option<Range<usize>>,
17}
18
19#[derive(Clone, Copy, Debug, PartialEq, Eq)]
21pub enum Severity {
22 Error,
24 Warning,
26}
27
28#[derive(Clone, Debug, PartialEq, Eq)]
30pub enum DiagnosticKind {
31 UndefinedNonTerminal {
33 name: String,
35 referenced_in: String,
37 },
38 LeftRecursion {
40 cycle: Vec<String>,
42 },
43 UnreachableRule {
45 name: String,
47 },
48 NullableRepetition {
50 rule: String,
52 detail: String,
54 },
55 DuplicateRule {
57 name: String,
59 },
60 UnusedCapture {
62 rule: String,
64 label: String,
66 },
67}
68
69impl DiagnosticKind {
70 #[must_use]
72 pub const fn as_str(&self) -> &'static str {
73 match self {
74 Self::UndefinedNonTerminal { .. } => "undefined-non-terminal",
75 Self::LeftRecursion { .. } => "left-recursion",
76 Self::UnreachableRule { .. } => "unreachable-rule",
77 Self::NullableRepetition { .. } => "nullable-repetition",
78 Self::DuplicateRule { .. } => "duplicate-rule",
79 Self::UnusedCapture { .. } => "unused-capture",
80 }
81 }
82}
83
84impl Severity {
85 #[must_use]
87 pub const fn as_str(self) -> &'static str {
88 match self {
89 Self::Error => "error",
90 Self::Warning => "warning",
91 }
92 }
93}
94
95#[derive(Clone, Debug, PartialEq, Eq)]
97pub struct GrammarDiagnostic {
98 pub kind: DiagnosticKind,
100 pub severity: Severity,
102 pub message: String,
104 pub location: RuleSpan,
106}
107
108impl GrammarDiagnostic {
109 #[must_use]
111 pub fn is_error(&self) -> bool {
112 self.severity == Severity::Error
113 }
114}
115
116impl fmt::Display for GrammarDiagnostic {
117 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
118 formatter.write_str(&self.message)
119 }
120}
121
122impl Error for GrammarDiagnostic {}
123
124impl Grammar {
125 #[must_use]
127 pub fn validate(&self) -> Vec<GrammarDiagnostic> {
128 validate(self)
129 }
130}
131
132pub const GRAMMAR_DIAGNOSTIC_KINDS: [&str; 6] = [
135 "duplicate-rule",
136 "undefined-non-terminal",
137 "left-recursion",
138 "unreachable-rule",
139 "nullable-repetition",
140 "unused-capture",
141];
142
143#[must_use]
146pub fn display_grammar_expression(expr: &GrammarExpr) -> String {
147 expr.to_string()
148}
149
150#[must_use]
152pub fn validate(grammar: &Grammar) -> Vec<GrammarDiagnostic> {
153 let defined_names = defined_rule_names(grammar);
154 let nullability = compute_nullability(grammar);
155
156 let mut diagnostics = Vec::new();
157 diagnostics.extend(check_duplicate_rules(grammar));
158 diagnostics.extend(check_undefined_nonterminals(grammar, &defined_names));
159 diagnostics.extend(check_left_recursion(grammar, &nullability));
160 diagnostics.extend(check_unreachable_rules(grammar));
161 diagnostics.extend(check_nullable_repetition(grammar, &nullability));
162 diagnostics.extend(check_unused_captures(grammar));
163
164 sort_diagnostics(grammar, &mut diagnostics);
165 diagnostics
166}
167
168fn check_duplicate_rules(grammar: &Grammar) -> Vec<GrammarDiagnostic> {
169 let mut counts = BTreeMap::<String, usize>::new();
170 for rule in grammar.rules() {
171 *counts.entry(rule.name.clone()).or_default() += 1;
172 }
173
174 counts
175 .into_iter()
176 .filter(|(_, count)| *count > 1)
177 .map(|(name, _)| {
178 let message = format!(
179 "rule `{name}` is defined more than once; merge alternatives into one rule or rename the duplicate."
180 );
181 GrammarDiagnostic {
182 kind: DiagnosticKind::DuplicateRule { name: name.clone() },
183 severity: Severity::Error,
184 message,
185 location: rule_location(name),
186 }
187 })
188 .collect()
189}
190
191fn check_undefined_nonterminals(
192 grammar: &Grammar,
193 defined_names: &[String],
194) -> Vec<GrammarDiagnostic> {
195 let defined = defined_names
196 .iter()
197 .map(String::as_str)
198 .collect::<BTreeSet<_>>();
199 let mut diagnostics = Vec::new();
200
201 for rule in grammar.rules() {
202 for name in collect_nonterminals(rule.expr()) {
203 if defined.contains(name.as_str()) {
204 continue;
205 }
206
207 let suggestion = nearest_rule_name(&name, defined_names);
208 let message = suggestion.map_or_else(
209 || {
210 format!(
211 "rule `{}` references undefined non-terminal `{name}`; define it or fix the spelling.",
212 rule.name()
213 )
214 },
215 |candidate| {
216 format!(
217 "rule `{}` references undefined non-terminal `{name}`; did you mean `{candidate}`? Define it or fix the spelling.",
218 rule.name()
219 )
220 },
221 );
222 diagnostics.push(GrammarDiagnostic {
223 kind: DiagnosticKind::UndefinedNonTerminal {
224 name,
225 referenced_in: rule.name.clone(),
226 },
227 severity: Severity::Error,
228 message,
229 location: rule_location(rule.name()),
230 });
231 }
232 }
233
234 diagnostics
235}
236
237fn check_left_recursion(
238 grammar: &Grammar,
239 nullability: &BTreeMap<String, bool>,
240) -> Vec<GrammarDiagnostic> {
241 let graph = left_reference_graph(grammar, nullability);
242 let mut diagnostics = Vec::new();
243 let mut seen_cycles = BTreeSet::new();
244
245 for rule in grammar.rules() {
246 let mut path = vec![rule.name.clone()];
247 let mut seen_rules = BTreeSet::from([rule.name.clone()]);
248 collect_left_cycles(
249 rule.name(),
250 rule.name(),
251 &graph,
252 &mut seen_rules,
253 &mut path,
254 &mut seen_cycles,
255 &mut diagnostics,
256 );
257 }
258
259 diagnostics
260}
261
262fn collect_left_cycles(
263 target: &str,
264 current: &str,
265 graph: &BTreeMap<String, BTreeSet<String>>,
266 seen_rules: &mut BTreeSet<String>,
267 path: &mut Vec<String>,
268 seen_cycles: &mut BTreeSet<String>,
269 diagnostics: &mut Vec<GrammarDiagnostic>,
270) {
271 let Some(next_rules) = graph.get(current) else {
272 return;
273 };
274
275 for next in next_rules {
276 if next == target {
277 let mut cycle = path.clone();
278 cycle.push(target.to_string());
279 if seen_cycles.insert(canonical_cycle_key(&cycle)) {
280 let cycle_text = cycle.join(" -> ");
281 diagnostics.push(GrammarDiagnostic {
282 kind: DiagnosticKind::LeftRecursion {
283 cycle: cycle.clone(),
284 },
285 severity: Severity::Error,
286 message: format!(
287 "rule `{target}` is left-recursive (`{cycle_text}`); a recursive-descent/PEG parser may not terminate. Rewrite using repetition or factor the common prefix."
288 ),
289 location: rule_location(target),
290 });
291 }
292 } else if graph.contains_key(next) && seen_rules.insert(next.clone()) {
293 path.push(next.clone());
294 collect_left_cycles(
295 target,
296 next,
297 graph,
298 seen_rules,
299 path,
300 seen_cycles,
301 diagnostics,
302 );
303 path.pop();
304 seen_rules.remove(next);
305 }
306 }
307}
308
309fn check_unreachable_rules(grammar: &Grammar) -> Vec<GrammarDiagnostic> {
310 let Some(start_rule) = grammar.start_rule() else {
311 return Vec::new();
312 };
313
314 let defined = grammar
315 .rules()
316 .iter()
317 .map(GrammarRule::name)
318 .collect::<BTreeSet<_>>();
319 let mut reachable = BTreeSet::new();
320 let mut stack = vec![start_rule.name.clone()];
321
322 while let Some(name) = stack.pop() {
323 if !reachable.insert(name.clone()) {
324 continue;
325 }
326
327 if let Some(rule) = grammar.rule(&name) {
328 for reference in collect_nonterminals(rule.expr()) {
329 if defined.contains(reference.as_str()) {
330 stack.push(reference);
331 }
332 }
333 }
334 }
335
336 grammar
337 .rules()
338 .iter()
339 .filter(|rule| !reachable.contains(rule.name()))
340 .map(|rule| GrammarDiagnostic {
341 kind: DiagnosticKind::UnreachableRule {
342 name: rule.name.clone(),
343 },
344 severity: Severity::Warning,
345 message: format!(
346 "rule `{}` is not reachable from start rule `{}`; remove it or reference it from a reachable rule.",
347 rule.name(),
348 start_rule.name()
349 ),
350 location: rule_location(rule.name()),
351 })
352 .collect()
353}
354
355fn check_nullable_repetition(
356 grammar: &Grammar,
357 nullability: &BTreeMap<String, bool>,
358) -> Vec<GrammarDiagnostic> {
359 let suspicious_nullable = compute_suspicious_nullability(grammar, nullability);
360 let mut diagnostics = Vec::new();
361
362 for rule in grammar.rules() {
363 if suspicious_nullable
364 .get(rule.name())
365 .copied()
366 .unwrap_or(false)
367 {
368 diagnostics.push(GrammarDiagnostic {
369 kind: DiagnosticKind::NullableRepetition {
370 rule: rule.name.clone(),
371 detail: "rule body is nullable".to_string(),
372 },
373 severity: Severity::Warning,
374 message: format!(
375 "rule `{}` can match empty; add a required terminal/non-terminal or document the rule as intentional.",
376 rule.name()
377 ),
378 location: rule_location(rule.name()),
379 });
380 }
381 collect_nullable_repetitions(rule.name(), rule.expr(), nullability, &mut diagnostics);
382 }
383
384 diagnostics
385}
386
387fn collect_nullable_repetitions(
388 rule_name: &str,
389 expr: &GrammarExpr,
390 nullability: &BTreeMap<String, bool>,
391 diagnostics: &mut Vec<GrammarDiagnostic>,
392) {
393 match expr {
394 GrammarExpr::ZeroOrMore(inner) => {
395 push_nullable_repetition(
396 rule_name,
397 "zero-or-more repetition",
398 inner,
399 nullability,
400 diagnostics,
401 );
402 collect_nullable_repetitions(rule_name, inner, nullability, diagnostics);
403 }
404 GrammarExpr::OneOrMore(inner) => {
405 push_nullable_repetition(
406 rule_name,
407 "one-or-more repetition",
408 inner,
409 nullability,
410 diagnostics,
411 );
412 collect_nullable_repetitions(rule_name, inner, nullability, diagnostics);
413 }
414 GrammarExpr::Repeat { expr: inner, .. } => {
415 push_nullable_repetition(
416 rule_name,
417 "counted repetition",
418 inner,
419 nullability,
420 diagnostics,
421 );
422 collect_nullable_repetitions(rule_name, inner, nullability, diagnostics);
423 }
424 GrammarExpr::Choice { alternatives, .. } => {
425 for alternative in alternatives {
426 collect_nullable_repetitions(rule_name, alternative, nullability, diagnostics);
427 }
428 }
429 GrammarExpr::Sequence(items) => {
430 for item in items {
431 collect_nullable_repetitions(rule_name, item, nullability, diagnostics);
432 }
433 }
434 GrammarExpr::Optional(inner)
435 | GrammarExpr::And(inner)
436 | GrammarExpr::Not(inner)
437 | GrammarExpr::Capture { expr: inner, .. } => {
438 collect_nullable_repetitions(rule_name, inner, nullability, diagnostics);
439 }
440 GrammarExpr::Feature(feature) => feature.for_each_expression(&mut |inner| {
441 collect_nullable_repetitions(rule_name, inner, nullability, diagnostics);
442 }),
443 GrammarExpr::Empty
444 | GrammarExpr::Terminal(_)
445 | GrammarExpr::TerminalInsensitive(_)
446 | GrammarExpr::CharRange(_, _)
447 | GrammarExpr::CharClass { .. }
448 | GrammarExpr::AnyChar
449 | GrammarExpr::NonTerminal(_) => {}
450 }
451}
452
453fn push_nullable_repetition(
454 rule_name: &str,
455 repetition: &str,
456 inner: &GrammarExpr,
457 nullability: &BTreeMap<String, bool>,
458 diagnostics: &mut Vec<GrammarDiagnostic>,
459) {
460 if !expr_is_nullable(inner, nullability) {
461 return;
462 }
463
464 let detail = format!("{repetition} has nullable inner expression `{inner}`");
465 diagnostics.push(GrammarDiagnostic {
466 kind: DiagnosticKind::NullableRepetition {
467 rule: rule_name.to_string(),
468 detail: detail.clone(),
469 },
470 severity: Severity::Warning,
471 message: format!(
472 "rule `{rule_name}` uses a {detail}; make the repeated expression consume input or move the optional part outside the repetition."
473 ),
474 location: rule_location(rule_name),
475 });
476}
477
478fn check_unused_captures(grammar: &Grammar) -> Vec<GrammarDiagnostic> {
479 let mut diagnostics = Vec::new();
480 for rule in grammar.rules() {
481 collect_unused_captures(rule.name(), rule.expr(), &mut diagnostics);
482 }
483 diagnostics
484}
485
486fn collect_unused_captures(
487 rule_name: &str,
488 expr: &GrammarExpr,
489 diagnostics: &mut Vec<GrammarDiagnostic>,
490) {
491 match expr {
492 GrammarExpr::Capture {
493 label: Some(label),
494 expr,
495 } => {
496 diagnostics.push(GrammarDiagnostic {
497 kind: DiagnosticKind::UnusedCapture {
498 rule: rule_name.to_string(),
499 label: label.clone(),
500 },
501 severity: Severity::Warning,
502 message: format!(
503 "capture label `{label}` in rule `{rule_name}` is not used by grammar semantics; remove the label or wire it to a consumer."
504 ),
505 location: rule_location(rule_name),
506 });
507 collect_unused_captures(rule_name, expr, diagnostics);
508 }
509 GrammarExpr::Capture { label: None, expr }
510 | GrammarExpr::Optional(expr)
511 | GrammarExpr::ZeroOrMore(expr)
512 | GrammarExpr::OneOrMore(expr)
513 | GrammarExpr::And(expr)
514 | GrammarExpr::Not(expr)
515 | GrammarExpr::Repeat { expr, .. } => collect_unused_captures(rule_name, expr, diagnostics),
516 GrammarExpr::Choice { alternatives, .. } => {
517 for alternative in alternatives {
518 collect_unused_captures(rule_name, alternative, diagnostics);
519 }
520 }
521 GrammarExpr::Sequence(items) => {
522 for item in items {
523 collect_unused_captures(rule_name, item, diagnostics);
524 }
525 }
526 GrammarExpr::Feature(feature) => feature.for_each_expression(&mut |inner| {
527 collect_unused_captures(rule_name, inner, diagnostics);
528 }),
529 GrammarExpr::Empty
530 | GrammarExpr::Terminal(_)
531 | GrammarExpr::TerminalInsensitive(_)
532 | GrammarExpr::CharRange(_, _)
533 | GrammarExpr::CharClass { .. }
534 | GrammarExpr::AnyChar
535 | GrammarExpr::NonTerminal(_) => {}
536 }
537}
538
539fn left_reference_graph(
540 grammar: &Grammar,
541 nullability: &BTreeMap<String, bool>,
542) -> BTreeMap<String, BTreeSet<String>> {
543 let defined = grammar
544 .rules()
545 .iter()
546 .map(GrammarRule::name)
547 .collect::<BTreeSet<_>>();
548 let mut graph = BTreeMap::<String, BTreeSet<String>>::new();
549
550 for rule in grammar.rules() {
551 let mut references = BTreeSet::new();
552 collect_left_references(rule.expr(), nullability, &mut references);
553 graph.entry(rule.name.clone()).or_default().extend(
554 references
555 .into_iter()
556 .filter(|name| defined.contains(name.as_str())),
557 );
558 }
559
560 graph
561}
562
563fn collect_left_references(
564 expr: &GrammarExpr,
565 nullability: &BTreeMap<String, bool>,
566 references: &mut BTreeSet<String>,
567) {
568 match expr {
569 GrammarExpr::NonTerminal(name) => {
570 references.insert(name.clone());
571 }
572 GrammarExpr::Choice { alternatives, .. } => {
573 for alternative in alternatives {
574 collect_left_references(alternative, nullability, references);
575 }
576 }
577 GrammarExpr::Sequence(items) => {
578 for item in items {
579 collect_left_references(item, nullability, references);
580 if !expr_is_nullable(item, nullability) {
581 break;
582 }
583 }
584 }
585 GrammarExpr::Optional(expr)
586 | GrammarExpr::ZeroOrMore(expr)
587 | GrammarExpr::OneOrMore(expr)
588 | GrammarExpr::And(expr)
589 | GrammarExpr::Not(expr)
590 | GrammarExpr::Capture { expr, .. }
591 | GrammarExpr::Repeat { expr, .. } => {
592 collect_left_references(expr, nullability, references);
593 }
594 GrammarExpr::Empty
595 | GrammarExpr::Terminal(_)
596 | GrammarExpr::TerminalInsensitive(_)
597 | GrammarExpr::CharRange(_, _)
598 | GrammarExpr::CharClass { .. }
599 | GrammarExpr::Feature(_)
600 | GrammarExpr::AnyChar => {}
601 }
602}
603
604fn compute_nullability(grammar: &Grammar) -> BTreeMap<String, bool> {
605 let mut nullability = grammar
606 .rules()
607 .iter()
608 .map(|rule| (rule.name.clone(), false))
609 .collect::<BTreeMap<_, _>>();
610
611 loop {
612 let mut changed = false;
613 for rule in grammar.rules() {
614 if !expr_is_nullable(rule.expr(), &nullability) {
615 continue;
616 }
617
618 let entry = nullability.entry(rule.name.clone()).or_insert(false);
619 if !*entry {
620 *entry = true;
621 changed = true;
622 }
623 }
624
625 if !changed {
626 return nullability;
627 }
628 }
629}
630
631fn expr_is_nullable(expr: &GrammarExpr, nullability: &BTreeMap<String, bool>) -> bool {
632 match expr {
633 GrammarExpr::Terminal(value) | GrammarExpr::TerminalInsensitive(value) => value.is_empty(),
634 GrammarExpr::CharRange(_, _)
635 | GrammarExpr::CharClass { .. }
636 | GrammarExpr::AnyChar
637 | GrammarExpr::Feature(_) => false,
638 GrammarExpr::NonTerminal(name) => nullability.get(name).copied().unwrap_or(false),
639 GrammarExpr::Choice { alternatives, .. } => alternatives
640 .iter()
641 .any(|alternative| expr_is_nullable(alternative, nullability)),
642 GrammarExpr::Sequence(items) => {
643 items.iter().all(|item| expr_is_nullable(item, nullability))
644 }
645 GrammarExpr::Empty
646 | GrammarExpr::Optional(_)
647 | GrammarExpr::ZeroOrMore(_)
648 | GrammarExpr::And(_)
649 | GrammarExpr::Not(_) => true,
650 GrammarExpr::OneOrMore(expr) | GrammarExpr::Capture { expr, .. } => {
651 expr_is_nullable(expr, nullability)
652 }
653 GrammarExpr::Repeat { expr, min, .. } => *min == 0 || expr_is_nullable(expr, nullability),
654 }
655}
656
657fn compute_suspicious_nullability(
658 grammar: &Grammar,
659 nullability: &BTreeMap<String, bool>,
660) -> BTreeMap<String, bool> {
661 let mut suspicious = grammar
662 .rules()
663 .iter()
664 .map(|rule| (rule.name.clone(), false))
665 .collect::<BTreeMap<_, _>>();
666
667 loop {
668 let mut changed = false;
669 for rule in grammar.rules() {
670 if !expr_is_suspicious_nullable(rule.expr(), nullability, &suspicious) {
671 continue;
672 }
673
674 let entry = suspicious.entry(rule.name.clone()).or_insert(false);
675 if !*entry {
676 *entry = true;
677 changed = true;
678 }
679 }
680
681 if !changed {
682 return suspicious;
683 }
684 }
685}
686
687fn expr_is_suspicious_nullable(
688 expr: &GrammarExpr,
689 nullability: &BTreeMap<String, bool>,
690 suspicious: &BTreeMap<String, bool>,
691) -> bool {
692 match expr {
693 GrammarExpr::Terminal(value) | GrammarExpr::TerminalInsensitive(value) => value.is_empty(),
694 GrammarExpr::CharRange(_, _)
695 | GrammarExpr::CharClass { .. }
696 | GrammarExpr::AnyChar
697 | GrammarExpr::Feature(_) => false,
698 GrammarExpr::NonTerminal(name) => suspicious.get(name).copied().unwrap_or(false),
699 GrammarExpr::Choice { alternatives, .. } => alternatives
700 .iter()
701 .any(|alternative| expr_is_suspicious_nullable(alternative, nullability, suspicious)),
702 GrammarExpr::Sequence(items) => {
703 items.is_empty()
704 || (items.iter().all(|item| expr_is_nullable(item, nullability))
705 && items
706 .iter()
707 .any(|item| expr_is_suspicious_nullable(item, nullability, suspicious)))
708 }
709 GrammarExpr::Empty
710 | GrammarExpr::Optional(_)
711 | GrammarExpr::And(_)
712 | GrammarExpr::Not(_) => true,
713 GrammarExpr::ZeroOrMore(expr) | GrammarExpr::OneOrMore(expr) => {
714 expr_is_nullable(expr, nullability)
715 }
716 GrammarExpr::Repeat { expr, .. } => expr_is_nullable(expr, nullability),
717 GrammarExpr::Capture { expr, .. } => {
718 expr_is_suspicious_nullable(expr, nullability, suspicious)
719 }
720 }
721}
722
723fn collect_nonterminals(expr: &GrammarExpr) -> BTreeSet<String> {
724 let mut names = BTreeSet::new();
725 collect_nonterminals_into(expr, &mut names);
726 names
727}
728
729fn collect_nonterminals_into(expr: &GrammarExpr, names: &mut BTreeSet<String>) {
730 match expr {
731 GrammarExpr::NonTerminal(name) => {
732 names.insert(name.clone());
733 }
734 GrammarExpr::Choice { alternatives, .. } => {
735 for alternative in alternatives {
736 collect_nonterminals_into(alternative, names);
737 }
738 }
739 GrammarExpr::Sequence(items) => {
740 for item in items {
741 collect_nonterminals_into(item, names);
742 }
743 }
744 GrammarExpr::Optional(expr)
745 | GrammarExpr::ZeroOrMore(expr)
746 | GrammarExpr::OneOrMore(expr)
747 | GrammarExpr::And(expr)
748 | GrammarExpr::Not(expr)
749 | GrammarExpr::Capture { expr, .. }
750 | GrammarExpr::Repeat { expr, .. } => collect_nonterminals_into(expr, names),
751 GrammarExpr::Feature(feature) => feature.collect_references(names),
752 GrammarExpr::Empty
753 | GrammarExpr::Terminal(_)
754 | GrammarExpr::TerminalInsensitive(_)
755 | GrammarExpr::CharRange(_, _)
756 | GrammarExpr::CharClass { .. }
757 | GrammarExpr::AnyChar => {}
758 }
759}
760
761fn defined_rule_names(grammar: &Grammar) -> Vec<String> {
762 let mut names = Vec::new();
763 let mut seen = BTreeSet::new();
764 for rule in grammar.rules() {
765 if seen.insert(rule.name.clone()) {
766 names.push(rule.name.clone());
767 }
768 }
769 names
770}
771
772fn nearest_rule_name<'a>(name: &str, candidates: &'a [String]) -> Option<&'a str> {
773 let mut best = None;
774
775 for candidate in candidates {
776 let distance = levenshtein(name, candidate);
777 if distance > 2 {
778 continue;
779 }
780
781 let should_replace = best.is_none_or(|(best_name, best_distance)| {
782 distance < best_distance
783 || (distance == best_distance && candidate.as_str() < best_name)
784 });
785 if should_replace {
786 best = Some((candidate.as_str(), distance));
787 }
788 }
789
790 best.map(|(candidate, _)| candidate)
791}
792
793fn levenshtein(left: &str, right: &str) -> usize {
794 let left_chars = left.chars().collect::<Vec<_>>();
795 let right_chars = right.chars().collect::<Vec<_>>();
796 let mut previous = (0..=right_chars.len()).collect::<Vec<_>>();
797 let mut current = vec![0; right_chars.len() + 1];
798
799 for (left_index, left_char) in left_chars.iter().enumerate() {
800 current[0] = left_index + 1;
801 for (right_index, right_char) in right_chars.iter().enumerate() {
802 let substitution_cost = usize::from(left_char != right_char);
803 current[right_index + 1] = (previous[right_index + 1] + 1)
804 .min(current[right_index] + 1)
805 .min(previous[right_index] + substitution_cost);
806 }
807 previous.clone_from(¤t);
808 }
809
810 previous[right_chars.len()]
811}
812
813fn canonical_cycle_key(cycle: &[String]) -> String {
814 let nodes = &cycle[..cycle.len().saturating_sub(1)];
815 if nodes.is_empty() {
816 return String::new();
817 }
818
819 let mut best = None;
820 for start in 0..nodes.len() {
821 let rotation = (0..nodes.len())
822 .map(|offset| nodes[(start + offset) % nodes.len()].as_str())
823 .collect::<Vec<_>>()
824 .join("\0");
825 if best.as_ref().is_none_or(|candidate| rotation < *candidate) {
826 best = Some(rotation);
827 }
828 }
829 best.unwrap_or_default()
830}
831
832fn sort_diagnostics(grammar: &Grammar, diagnostics: &mut [GrammarDiagnostic]) {
833 let indices = grammar
834 .rules()
835 .iter()
836 .enumerate()
837 .rev()
838 .map(|(index, rule)| (rule.name.clone(), index))
839 .collect::<BTreeMap<_, _>>();
840
841 diagnostics.sort_by(|left, right| {
842 let left_span = left
843 .location
844 .span
845 .as_ref()
846 .map_or(usize::MAX, |span| span.start);
847 let right_span = right
848 .location
849 .span
850 .as_ref()
851 .map_or(usize::MAX, |span| span.start);
852 let left_index = indices
853 .get(&left.location.rule)
854 .copied()
855 .unwrap_or(usize::MAX);
856 let right_index = indices
857 .get(&right.location.rule)
858 .copied()
859 .unwrap_or(usize::MAX);
860
861 left_span
862 .cmp(&right_span)
863 .then_with(|| left_index.cmp(&right_index))
864 .then_with(|| left.location.rule.cmp(&right.location.rule))
865 .then_with(|| severity_rank(left.severity).cmp(&severity_rank(right.severity)))
866 .then_with(|| kind_rank(&left.kind).cmp(&kind_rank(&right.kind)))
867 .then_with(|| left.message.cmp(&right.message))
868 });
869}
870
871const fn severity_rank(severity: Severity) -> usize {
872 match severity {
873 Severity::Error => 0,
874 Severity::Warning => 1,
875 }
876}
877
878const fn kind_rank(kind: &DiagnosticKind) -> usize {
879 match kind {
880 DiagnosticKind::DuplicateRule { .. } => 0,
881 DiagnosticKind::UndefinedNonTerminal { .. } => 1,
882 DiagnosticKind::LeftRecursion { .. } => 2,
883 DiagnosticKind::UnreachableRule { .. } => 3,
884 DiagnosticKind::NullableRepetition { .. } => 4,
885 DiagnosticKind::UnusedCapture { .. } => 5,
886 }
887}
888
889fn rule_location(rule: impl Into<String>) -> RuleSpan {
890 RuleSpan {
891 rule: rule.into(),
892 span: None,
893 }
894}