1use ariadne::{Color, ColorGenerator, Config, Fmt, Label, Report, ReportKind, Source};
11use serde::{Deserialize, Serialize};
12use std::ops::Range;
13use strsim::jaro_winkler;
14
15use crate::span::Span;
16use crate::lexer::Token;
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
20#[serde(rename_all = "lowercase")]
21pub enum Severity {
22 Error,
23 Warning,
24 Info,
25 Hint,
26}
27
28impl Severity {
29 fn to_report_kind(self) -> ReportKind<'static> {
30 match self {
31 Severity::Error => ReportKind::Error,
32 Severity::Warning => ReportKind::Warning,
33 Severity::Info => ReportKind::Advice,
34 Severity::Hint => ReportKind::Advice,
35 }
36 }
37
38 fn color(self) -> Color {
39 match self {
40 Severity::Error => Color::Red,
41 Severity::Warning => Color::Yellow,
42 Severity::Info => Color::Blue,
43 Severity::Hint => Color::Cyan,
44 }
45 }
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct FixSuggestion {
51 pub message: String,
52 pub span: Span,
53 pub replacement: String,
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct RelatedInfo {
59 pub message: String,
60 pub span: Span,
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize)]
65pub struct DiagnosticLabel {
66 pub span: Span,
67 pub message: String,
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct Diagnostic {
73 pub severity: Severity,
74 pub code: Option<String>,
75 pub message: String,
76 pub span: Span,
77 #[serde(skip)]
78 pub labels: Vec<(Span, String)>,
79 pub notes: Vec<String>,
80 pub suggestions: Vec<FixSuggestion>,
81 pub related: Vec<RelatedInfo>,
82}
83
84#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct JsonDiagnostic {
87 pub severity: Severity,
88 pub code: Option<String>,
89 pub message: String,
90 pub file: String,
91 pub span: Span,
92 pub line: u32,
93 pub column: u32,
94 pub end_line: u32,
95 pub end_column: u32,
96 pub labels: Vec<DiagnosticLabel>,
97 pub notes: Vec<String>,
98 pub suggestions: Vec<FixSuggestion>,
99 pub related: Vec<RelatedInfo>,
100}
101
102#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct JsonDiagnosticsOutput {
105 pub file: String,
106 pub diagnostics: Vec<JsonDiagnostic>,
107 pub error_count: usize,
108 pub warning_count: usize,
109 pub success: bool,
110}
111
112impl Diagnostic {
113 pub fn error(message: impl Into<String>, span: Span) -> Self {
115 Self {
116 severity: Severity::Error,
117 code: None,
118 message: message.into(),
119 span,
120 labels: Vec::new(),
121 notes: Vec::new(),
122 suggestions: Vec::new(),
123 related: Vec::new(),
124 }
125 }
126
127 pub fn warning(message: impl Into<String>, span: Span) -> Self {
129 Self {
130 severity: Severity::Warning,
131 code: None,
132 message: message.into(),
133 span,
134 labels: Vec::new(),
135 notes: Vec::new(),
136 suggestions: Vec::new(),
137 related: Vec::new(),
138 }
139 }
140
141 pub fn with_code(mut self, code: impl Into<String>) -> Self {
143 self.code = Some(code.into());
144 self
145 }
146
147 pub fn with_label(mut self, span: Span, message: impl Into<String>) -> Self {
149 self.labels.push((span, message.into()));
150 self
151 }
152
153 pub fn with_note(mut self, note: impl Into<String>) -> Self {
155 self.notes.push(note.into());
156 self
157 }
158
159 pub fn with_suggestion(mut self, message: impl Into<String>, span: Span, replacement: impl Into<String>) -> Self {
161 self.suggestions.push(FixSuggestion {
162 message: message.into(),
163 span,
164 replacement: replacement.into(),
165 });
166 self
167 }
168
169 pub fn with_related(mut self, message: impl Into<String>, span: Span) -> Self {
171 self.related.push(RelatedInfo {
172 message: message.into(),
173 span,
174 });
175 self
176 }
177
178 pub fn render(&self, filename: &str, source: &str) -> String {
180 let mut output = Vec::new();
181 self.write_to(&mut output, filename, source);
182 String::from_utf8(output).unwrap_or_else(|_| self.message.clone())
183 }
184
185 pub fn write_to<W: std::io::Write>(&self, writer: W, filename: &str, source: &str) {
187 let span_range: Range<usize> = self.span.start..self.span.end;
188
189 let mut colors = ColorGenerator::new();
190 let primary_color = self.severity.color();
191
192 let mut builder = Report::build(self.severity.to_report_kind(), filename, self.span.start)
193 .with_config(Config::default().with_cross_gap(true))
194 .with_message(&self.message);
195
196 if let Some(ref code) = self.code {
198 builder = builder.with_code(code);
199 }
200
201 builder = builder.with_label(
203 Label::new((filename, span_range.clone()))
204 .with_message(&self.message)
205 .with_color(primary_color),
206 );
207
208 for (span, msg) in &self.labels {
210 let color = colors.next();
211 builder = builder.with_label(
212 Label::new((filename, span.start..span.end))
213 .with_message(msg)
214 .with_color(color),
215 );
216 }
217
218 for note in &self.notes {
220 builder = builder.with_note(note);
221 }
222
223 for suggestion in &self.suggestions {
225 let help_msg = format!(
226 "help: {}: `{}`",
227 suggestion.message,
228 suggestion.replacement.clone().fg(Color::Green)
229 );
230 builder = builder.with_help(help_msg);
231 }
232
233 builder
234 .finish()
235 .write((filename, Source::from(source)), writer)
236 .unwrap();
237 }
238
239 pub fn eprint(&self, filename: &str, source: &str) {
241 self.write_to(std::io::stderr(), filename, source);
242 }
243
244 pub fn to_json(&self, filename: &str, source: &str) -> JsonDiagnostic {
246 let (line, column) = offset_to_line_col(source, self.span.start);
247 let (end_line, end_column) = offset_to_line_col(source, self.span.end);
248
249 JsonDiagnostic {
250 severity: self.severity,
251 code: self.code.clone(),
252 message: self.message.clone(),
253 file: filename.to_string(),
254 span: self.span,
255 line,
256 column,
257 end_line,
258 end_column,
259 labels: self.labels.iter().map(|(span, msg)| DiagnosticLabel {
260 span: *span,
261 message: msg.clone(),
262 }).collect(),
263 notes: self.notes.clone(),
264 suggestions: self.suggestions.clone(),
265 related: self.related.clone(),
266 }
267 }
268}
269
270fn offset_to_line_col(source: &str, offset: usize) -> (u32, u32) {
272 let mut line = 1u32;
273 let mut col = 1u32;
274
275 for (i, ch) in source.char_indices() {
276 if i >= offset {
277 break;
278 }
279 if ch == '\n' {
280 line += 1;
281 col = 1;
282 } else {
283 col += 1;
284 }
285 }
286
287 (line, col)
288}
289
290pub struct DiagnosticBuilder;
292
293impl DiagnosticBuilder {
294 pub fn unexpected_token(
296 expected: &str,
297 found: &Token,
298 span: Span,
299 source: &str,
300 ) -> Diagnostic {
301 let found_str = format!("{:?}", found);
302 let message = format!("expected {}, found {}", expected, found_str);
303
304 let mut diag = Diagnostic::error(message, span)
305 .with_code("E0001");
306
307 diag = diag.with_label(span, format!("expected {} here", expected));
309
310 if let Some(suggestion) = Self::suggest_token_fix(expected, found, source, span) {
312 diag = diag.with_suggestion(
313 suggestion.0,
314 span,
315 suggestion.1,
316 );
317 }
318
319 diag
320 }
321
322 pub fn undefined_variable(
324 name: &str,
325 span: Span,
326 known_names: &[&str],
327 ) -> Diagnostic {
328 let message = format!("cannot find value `{}` in this scope", name);
329 let mut diag = Diagnostic::error(message, span)
330 .with_code("E0425")
331 .with_label(span, "not found in this scope");
332
333 if let Some(suggestion) = Self::find_similar(name, known_names) {
335 diag = diag.with_suggestion(
336 format!("a local variable with a similar name exists"),
337 span,
338 suggestion.to_string(),
339 );
340 }
341
342 diag
343 }
344
345 pub fn type_mismatch(
347 expected: &str,
348 found: &str,
349 span: Span,
350 expected_span: Option<Span>,
351 ) -> Diagnostic {
352 let message = format!("mismatched types: expected `{}`, found `{}`", expected, found);
353 let mut diag = Diagnostic::error(message, span)
354 .with_code("E0308")
355 .with_label(span, format!("expected `{}`", expected));
356
357 if let Some(exp_span) = expected_span {
358 diag = diag.with_related("expected due to this", exp_span);
359 }
360
361 diag
362 }
363
364 pub fn evidentiality_mismatch(
366 expected: &str,
367 found: &str,
368 span: Span,
369 ) -> Diagnostic {
370 let message = format!(
371 "evidentiality mismatch: expected `{}`, found `{}`",
372 expected, found
373 );
374
375 Diagnostic::error(message, span)
376 .with_code("E0600")
377 .with_label(span, format!("has evidentiality `{}`", found))
378 .with_note(format!(
379 "values with `{}` evidentiality cannot be used where `{}` is required",
380 found, expected
381 ))
382 .with_note(Self::evidentiality_help(expected, found))
383 }
384
385 pub fn untrusted_data_used(
387 span: Span,
388 source_span: Option<Span>,
389 ) -> Diagnostic {
390 let mut diag = Diagnostic::error(
391 "cannot use reported (~) data without validation",
392 span,
393 )
394 .with_code("E0601")
395 .with_label(span, "untrusted data used here")
396 .with_note("data from external sources must be validated before use")
397 .with_suggestion(
398 "validate the data first",
399 span,
400 "value|validate!{...}",
401 );
402
403 if let Some(src) = source_span {
404 diag = diag.with_related("data originates from external source here", src);
405 }
406
407 diag
408 }
409
410 pub fn unknown_morpheme(
412 found: &str,
413 span: Span,
414 ) -> Diagnostic {
415 let message = format!("unknown morpheme `{}`", found);
416 let mut diag = Diagnostic::error(message, span)
417 .with_code("E0100");
418
419 let morphemes = [
421 ("τ", "tau", "transform/map"),
422 ("φ", "phi", "filter"),
423 ("σ", "sigma", "sort"),
424 ("ρ", "rho", "reduce"),
425 ("λ", "lambda", "anonymous function"),
426 ("Σ", "sum", "sum all"),
427 ("Π", "pi", "product"),
428 ("α", "alpha", "first element"),
429 ("ω", "omega", "last element"),
430 ("μ", "mu", "middle element"),
431 ("χ", "chi", "random choice"),
432 ("ν", "nu", "nth element"),
433 ("ξ", "xi", "next in sequence"),
434 ];
435
436 if let Some((greek, _ascii, desc)) = morphemes.iter().find(|(g, a, _)| {
437 jaro_winkler(found, g) > 0.8 || jaro_winkler(found, a) > 0.8
438 }) {
439 diag = diag.with_suggestion(
440 format!("did you mean the {} morpheme?", desc),
441 span,
442 greek.to_string(),
443 );
444 }
445
446 diag = diag.with_note("transform morphemes: τ (map), φ (filter), σ (sort), ρ (reduce), Σ (sum), Π (product)");
447 diag = diag.with_note("access morphemes: α (first), ω (last), μ (middle), χ (choice), ν (nth), ξ (next)");
448
449 diag
450 }
451
452 pub fn suggest_unicode_symbol(ascii: &str) -> Option<(&'static str, &'static str)> {
455 match ascii {
456 "&&" => Some(("∧", "logical AND")),
458 "||" => Some(("∨", "logical OR")),
459 "^^" => Some(("⊻", "logical XOR")),
460
461 "&" => Some(("⋏", "bitwise AND")),
463 "|" => Some(("⋎", "bitwise OR")),
464
465 "union" => Some(("∪", "set union")),
467 "intersect" | "intersection" => Some(("∩", "set intersection")),
468 "subset" => Some(("⊂", "proper subset")),
469 "superset" => Some(("⊃", "proper superset")),
470 "in" | "element_of" => Some(("∈", "element of")),
471 "not_in" => Some(("∉", "not element of")),
472
473 "sqrt" => Some(("√", "square root")),
475 "cbrt" => Some(("∛", "cube root")),
476 "infinity" | "inf" => Some(("∞", "infinity")),
477 "pi" => Some(("π", "pi constant")),
478 "sum" => Some(("Σ", "summation")),
479 "product" => Some(("Π", "product")),
480 "integral" => Some(("∫", "integral/cumulative sum")),
481 "partial" | "derivative" => Some(("∂", "partial/derivative")),
482
483 "tau" | "map" | "transform" => Some(("τ", "transform morpheme")),
485 "phi" | "filter" => Some(("φ", "filter morpheme")),
486 "sigma" | "sort" => Some(("σ", "sort morpheme")),
487 "rho" | "reduce" | "fold" => Some(("ρ", "reduce morpheme")),
488 "lambda" => Some(("λ", "lambda")),
489 "alpha" | "first" => Some(("α", "first element")),
490 "omega" | "last" => Some(("ω", "last element")),
491 "mu" | "middle" | "median" => Some(("μ", "middle element")),
492 "chi" | "choice" | "random" => Some(("χ", "random choice")),
493 "nu" | "nth" => Some(("ν", "nth element")),
494 "xi" | "next" => Some(("ξ", "next in sequence")),
495 "delta" | "diff" | "change" => Some(("δ", "delta/change")),
496 "epsilon" | "empty" => Some(("ε", "epsilon/empty")),
497 "zeta" | "zip" => Some(("ζ", "zeta/zip")),
498
499 "compose" => Some(("∘", "function composition")),
501 "tensor" => Some(("⊗", "tensor product")),
502 "direct_sum" | "xor" => Some(("⊕", "direct sum/XOR")),
503
504 "null" | "void" | "nothing" => Some(("∅", "empty set")),
506 "true" | "top" | "any" => Some(("⊤", "top/true")),
507 "false" | "bottom" | "never" => Some(("⊥", "bottom/false")),
508
509 "forall" | "for_all" => Some(("∀", "universal quantifier")),
511 "exists" => Some(("∃", "existential quantifier")),
512
513 "join" | "zip_with" => Some(("⋈", "join/zip with")),
515 "flatten" => Some(("⋳", "flatten")),
516 "max" | "supremum" => Some(("⊔", "supremum/max")),
517 "min" | "infimum" => Some(("⊓", "infimum/min")),
518
519 _ => None,
520 }
521 }
522
523 pub fn suggest_symbol_upgrade(
525 ascii: &str,
526 span: Span,
527 ) -> Option<Diagnostic> {
528 Self::suggest_unicode_symbol(ascii).map(|(unicode, desc)| {
529 Diagnostic::warning(
530 format!("consider using Unicode symbol `{}` for {}", unicode, desc),
531 span,
532 )
533 .with_code("W0200")
534 .with_suggestion(
535 format!("use `{}` for clearer, more idiomatic Sigil", unicode),
536 span,
537 unicode.to_string(),
538 )
539 .with_note(format!(
540 "Sigil supports Unicode symbols. `{}` → `{}` ({})",
541 ascii, unicode, desc
542 ))
543 })
544 }
545
546 pub fn all_symbol_mappings() -> Vec<(&'static str, &'static str, &'static str)> {
548 vec![
549 ("&&", "∧", "logical AND"),
551 ("||", "∨", "logical OR"),
552 ("^^", "⊻", "logical XOR"),
553 ("&", "⋏", "bitwise AND"),
554 ("|", "⋎", "bitwise OR"),
555 ("union", "∪", "set union"),
556 ("intersect", "∩", "set intersection"),
557 ("subset", "⊂", "proper subset"),
558 ("superset", "⊃", "proper superset"),
559 ("in", "∈", "element of"),
560 ("not_in", "∉", "not element of"),
561 ("sqrt", "√", "square root"),
562 ("cbrt", "∛", "cube root"),
563 ("infinity", "∞", "infinity"),
564 ("tau", "τ", "transform"),
565 ("phi", "φ", "filter"),
566 ("sigma", "σ", "sort"),
567 ("rho", "ρ", "reduce"),
568 ("lambda", "λ", "lambda"),
569 ("alpha", "α", "first"),
570 ("omega", "ω", "last"),
571 ("mu", "μ", "middle"),
572 ("chi", "χ", "choice"),
573 ("nu", "ν", "nth"),
574 ("xi", "ξ", "next"),
575 ("sum", "Σ", "sum"),
576 ("product", "Π", "product"),
577 ("compose", "∘", "compose"),
578 ("tensor", "⊗", "tensor"),
579 ("xor", "⊕", "direct sum"),
580 ("forall", "∀", "for all"),
581 ("exists", "∃", "exists"),
582 ("null", "∅", "empty"),
583 ("true", "⊤", "top/true"),
584 ("false", "⊥", "bottom/false"),
585 ]
586 }
587
588 fn find_similar<'a>(name: &str, candidates: &[&'a str]) -> Option<&'a str> {
590 candidates
591 .iter()
592 .filter(|c| jaro_winkler(name, c) > 0.8)
593 .max_by(|a, b| {
594 jaro_winkler(name, a)
595 .partial_cmp(&jaro_winkler(name, b))
596 .unwrap_or(std::cmp::Ordering::Equal)
597 })
598 .copied()
599 }
600
601 fn suggest_token_fix(
603 expected: &str,
604 found: &Token,
605 _source: &str,
606 _span: Span,
607 ) -> Option<(String, String)> {
608 match (expected, found) {
610 ("`;`", Token::RBrace) => Some((
611 "you might be missing a semicolon".to_string(),
612 ";".to_string(),
613 )),
614 ("`{`", Token::Arrow) => Some((
615 "you might want a block here".to_string(),
616 "{ ... }".to_string(),
617 )),
618 ("`)`", Token::Comma) => Some((
619 "unexpected comma, maybe close the parenthesis first".to_string(),
620 ")".to_string(),
621 )),
622 _ => None,
623 }
624 }
625
626 fn evidentiality_help(expected: &str, found: &str) -> String {
628 match (expected, found) {
629 ("!", "~") => "use `value|validate!{...}` to promote reported data to known".to_string(),
630 ("!", "?") => "handle the uncertain case with `match` or unwrap with `value!`".to_string(),
631 ("?", "~") => "reported data is already uncertain, no conversion needed".to_string(),
632 _ => format!("evidentiality flows: ! (known) < ? (uncertain) < ~ (reported) < ‽ (paradox)"),
633 }
634 }
635}
636
637#[derive(Debug, Default)]
639pub struct Diagnostics {
640 items: Vec<Diagnostic>,
641 has_errors: bool,
642}
643
644impl Diagnostics {
645 pub fn new() -> Self {
646 Self::default()
647 }
648
649 pub fn add(&mut self, diagnostic: Diagnostic) {
650 if diagnostic.severity == Severity::Error {
651 self.has_errors = true;
652 }
653 self.items.push(diagnostic);
654 }
655
656 pub fn has_errors(&self) -> bool {
657 self.has_errors
658 }
659
660 pub fn is_empty(&self) -> bool {
661 self.items.is_empty()
662 }
663
664 pub fn iter(&self) -> impl Iterator<Item = &Diagnostic> {
665 self.items.iter()
666 }
667
668 pub fn render_all(&self, filename: &str, source: &str) -> String {
670 let mut output = String::new();
671 for diag in &self.items {
672 output.push_str(&diag.render(filename, source));
673 output.push('\n');
674 }
675 output
676 }
677
678 pub fn eprint_all(&self, filename: &str, source: &str) {
680 for diag in &self.items {
681 diag.eprint(filename, source);
682 }
683 }
684
685 pub fn error_count(&self) -> usize {
687 self.items.iter().filter(|d| d.severity == Severity::Error).count()
688 }
689
690 pub fn warning_count(&self) -> usize {
692 self.items.iter().filter(|d| d.severity == Severity::Warning).count()
693 }
694
695 pub fn print_summary(&self) {
697 let errors = self.error_count();
698 let warnings = self.warning_count();
699
700 if errors > 0 || warnings > 0 {
701 eprint!("\n");
702 if errors > 0 {
703 eprintln!(
704 "{}: aborting due to {} previous error{}",
705 "error".fg(Color::Red),
706 errors,
707 if errors == 1 { "" } else { "s" }
708 );
709 }
710 if warnings > 0 {
711 eprintln!(
712 "{}: {} warning{} emitted",
713 "warning".fg(Color::Yellow),
714 warnings,
715 if warnings == 1 { "" } else { "s" }
716 );
717 }
718 }
719 }
720
721 pub fn to_json_output(&self, filename: &str, source: &str) -> JsonDiagnosticsOutput {
726 let diagnostics: Vec<JsonDiagnostic> = self.items
727 .iter()
728 .map(|d| d.to_json(filename, source))
729 .collect();
730
731 let error_count = self.error_count();
732 let warning_count = self.warning_count();
733
734 JsonDiagnosticsOutput {
735 file: filename.to_string(),
736 diagnostics,
737 error_count,
738 warning_count,
739 success: error_count == 0,
740 }
741 }
742
743 pub fn to_json_string(&self, filename: &str, source: &str) -> String {
747 let output = self.to_json_output(filename, source);
748 serde_json::to_string_pretty(&output).unwrap_or_else(|_| "{}".to_string())
749 }
750
751 pub fn to_json_compact(&self, filename: &str, source: &str) -> String {
755 let output = self.to_json_output(filename, source);
756 serde_json::to_string(&output).unwrap_or_else(|_| "{}".to_string())
757 }
758}
759
760#[cfg(test)]
761mod tests {
762 use super::*;
763
764 #[test]
765 fn test_undefined_variable_suggestion() {
766 let known = vec!["counter", "count", "total", "sum"];
767 let diag = DiagnosticBuilder::undefined_variable("countr", Span::new(10, 16), &known);
768
769 assert!(diag.suggestions.iter().any(|s| s.replacement == "counter"));
770 }
771
772 #[test]
773 fn test_evidentiality_mismatch() {
774 let diag = DiagnosticBuilder::evidentiality_mismatch("!", "~", Span::new(0, 5));
775
776 assert!(diag.notes.iter().any(|n| n.contains("validate")));
777 }
778
779 #[test]
780 fn test_unicode_symbol_suggestions() {
781 assert_eq!(DiagnosticBuilder::suggest_unicode_symbol("&&"), Some(("∧", "logical AND")));
783 assert_eq!(DiagnosticBuilder::suggest_unicode_symbol("||"), Some(("∨", "logical OR")));
784
785 assert_eq!(DiagnosticBuilder::suggest_unicode_symbol("&"), Some(("⋏", "bitwise AND")));
787 assert_eq!(DiagnosticBuilder::suggest_unicode_symbol("|"), Some(("⋎", "bitwise OR")));
788
789 assert_eq!(DiagnosticBuilder::suggest_unicode_symbol("tau"), Some(("τ", "transform morpheme")));
791 assert_eq!(DiagnosticBuilder::suggest_unicode_symbol("filter"), Some(("φ", "filter morpheme")));
792 assert_eq!(DiagnosticBuilder::suggest_unicode_symbol("alpha"), Some(("α", "first element")));
793
794 assert_eq!(DiagnosticBuilder::suggest_unicode_symbol("sqrt"), Some(("√", "square root")));
796 assert_eq!(DiagnosticBuilder::suggest_unicode_symbol("infinity"), Some(("∞", "infinity")));
797
798 assert_eq!(DiagnosticBuilder::suggest_unicode_symbol("foobar"), None);
800 }
801
802 #[test]
803 fn test_symbol_upgrade_diagnostic() {
804 let diag = DiagnosticBuilder::suggest_symbol_upgrade("&&", Span::new(0, 2));
805 assert!(diag.is_some());
806
807 let d = diag.unwrap();
808 assert!(d.suggestions.iter().any(|s| s.replacement == "∧"));
809 assert!(d.notes.iter().any(|n| n.contains("logical AND")));
810 }
811
812 #[test]
813 fn test_unknown_morpheme_with_access_morphemes() {
814 let diag = DiagnosticBuilder::unknown_morpheme("alph", Span::new(0, 4));
815
816 assert!(diag.suggestions.iter().any(|s| s.replacement == "α"));
818 assert!(diag.notes.iter().any(|n| n.contains("transform morphemes")));
820 assert!(diag.notes.iter().any(|n| n.contains("access morphemes")));
821 }
822
823 #[test]
824 fn test_all_symbol_mappings() {
825 let mappings = DiagnosticBuilder::all_symbol_mappings();
826 assert!(!mappings.is_empty());
827
828 assert!(mappings.iter().any(|(a, u, _)| *a == "&&" && *u == "∧"));
830 assert!(mappings.iter().any(|(a, u, _)| *a == "tau" && *u == "τ"));
831 assert!(mappings.iter().any(|(a, u, _)| *a == "sqrt" && *u == "√"));
832 }
833}