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::lexer::Token;
16use crate::span::Span;
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(
161 mut self,
162 message: impl Into<String>,
163 span: Span,
164 replacement: impl Into<String>,
165 ) -> Self {
166 self.suggestions.push(FixSuggestion {
167 message: message.into(),
168 span,
169 replacement: replacement.into(),
170 });
171 self
172 }
173
174 pub fn with_related(mut self, message: impl Into<String>, span: Span) -> Self {
176 self.related.push(RelatedInfo {
177 message: message.into(),
178 span,
179 });
180 self
181 }
182
183 pub fn render(&self, filename: &str, source: &str) -> String {
185 let mut output = Vec::new();
186 self.write_to(&mut output, filename, source);
187 String::from_utf8(output).unwrap_or_else(|_| self.message.clone())
188 }
189
190 pub fn write_to<W: std::io::Write>(&self, writer: W, filename: &str, source: &str) {
192 let span_range: Range<usize> = self.span.start..self.span.end;
193
194 let mut colors = ColorGenerator::new();
195 let primary_color = self.severity.color();
196
197 let mut builder = Report::build(self.severity.to_report_kind(), filename, self.span.start)
198 .with_config(Config::default().with_cross_gap(true))
199 .with_message(&self.message);
200
201 if let Some(ref code) = self.code {
203 builder = builder.with_code(code);
204 }
205
206 builder = builder.with_label(
208 Label::new((filename, span_range.clone()))
209 .with_message(&self.message)
210 .with_color(primary_color),
211 );
212
213 for (span, msg) in &self.labels {
215 let color = colors.next();
216 builder = builder.with_label(
217 Label::new((filename, span.start..span.end))
218 .with_message(msg)
219 .with_color(color),
220 );
221 }
222
223 for note in &self.notes {
225 builder = builder.with_note(note);
226 }
227
228 for suggestion in &self.suggestions {
230 let help_msg = format!(
231 "help: {}: `{}`",
232 suggestion.message,
233 suggestion.replacement.clone().fg(Color::Green)
234 );
235 builder = builder.with_help(help_msg);
236 }
237
238 builder
239 .finish()
240 .write((filename, Source::from(source)), writer)
241 .unwrap();
242 }
243
244 pub fn eprint(&self, filename: &str, source: &str) {
246 self.write_to(std::io::stderr(), filename, source);
247 }
248
249 pub fn to_json(&self, filename: &str, source: &str) -> JsonDiagnostic {
251 let (line, column) = offset_to_line_col(source, self.span.start);
252 let (end_line, end_column) = offset_to_line_col(source, self.span.end);
253
254 JsonDiagnostic {
255 severity: self.severity,
256 code: self.code.clone(),
257 message: self.message.clone(),
258 file: filename.to_string(),
259 span: self.span,
260 line,
261 column,
262 end_line,
263 end_column,
264 labels: self
265 .labels
266 .iter()
267 .map(|(span, msg)| DiagnosticLabel {
268 span: *span,
269 message: msg.clone(),
270 })
271 .collect(),
272 notes: self.notes.clone(),
273 suggestions: self.suggestions.clone(),
274 related: self.related.clone(),
275 }
276 }
277}
278
279fn offset_to_line_col(source: &str, offset: usize) -> (u32, u32) {
281 let mut line = 1u32;
282 let mut col = 1u32;
283
284 for (i, ch) in source.char_indices() {
285 if i >= offset {
286 break;
287 }
288 if ch == '\n' {
289 line += 1;
290 col = 1;
291 } else {
292 col += 1;
293 }
294 }
295
296 (line, col)
297}
298
299pub struct DiagnosticBuilder;
301
302impl DiagnosticBuilder {
303 pub fn unexpected_token(expected: &str, found: &Token, span: Span, source: &str) -> Diagnostic {
305 let found_str = format!("{:?}", found);
306 let message = format!("expected {}, found {}", expected, found_str);
307
308 let mut diag = Diagnostic::error(message, span).with_code("E0001");
309
310 diag = diag.with_label(span, format!("expected {} here", expected));
312
313 if let Some(suggestion) = Self::suggest_token_fix(expected, found, source, span) {
315 diag = diag.with_suggestion(suggestion.0, span, suggestion.1);
316 }
317
318 diag
319 }
320
321 pub fn undefined_variable(name: &str, span: Span, known_names: &[&str]) -> Diagnostic {
323 let message = format!("cannot find value `{}` in this scope", name);
324 let mut diag = Diagnostic::error(message, span)
325 .with_code("E0425")
326 .with_label(span, "not found in this scope");
327
328 if let Some(suggestion) = Self::find_similar(name, known_names) {
330 diag = diag.with_suggestion(
331 format!("a local variable with a similar name exists"),
332 span,
333 suggestion.to_string(),
334 );
335 }
336
337 diag
338 }
339
340 pub fn type_mismatch(
342 expected: &str,
343 found: &str,
344 span: Span,
345 expected_span: Option<Span>,
346 ) -> Diagnostic {
347 let message = format!(
348 "mismatched types: expected `{}`, found `{}`",
349 expected, found
350 );
351 let mut diag = Diagnostic::error(message, span)
352 .with_code("E0308")
353 .with_label(span, format!("expected `{}`", expected));
354
355 if let Some(exp_span) = expected_span {
356 diag = diag.with_related("expected due to this", exp_span);
357 }
358
359 diag
360 }
361
362 pub fn evidentiality_mismatch(expected: &str, found: &str, span: Span) -> Diagnostic {
364 let message = format!(
365 "evidentiality mismatch: expected `{}`, found `{}`",
366 expected, found
367 );
368
369 Diagnostic::error(message, span)
370 .with_code("E0600")
371 .with_label(span, format!("has evidentiality `{}`", found))
372 .with_note(format!(
373 "values with `{}` evidentiality cannot be used where `{}` is required",
374 found, expected
375 ))
376 .with_note(Self::evidentiality_help(expected, found))
377 }
378
379 pub fn untrusted_data_used(span: Span, source_span: Option<Span>) -> Diagnostic {
381 let mut diag = Diagnostic::error("cannot use reported (~) data without validation", span)
382 .with_code("E0601")
383 .with_label(span, "untrusted data used here")
384 .with_note("data from external sources must be validated before use")
385 .with_suggestion("validate the data first", span, "value|validate!{...}");
386
387 if let Some(src) = source_span {
388 diag = diag.with_related("data originates from external source here", src);
389 }
390
391 diag
392 }
393
394 pub fn unknown_morpheme(found: &str, span: Span) -> Diagnostic {
396 let message = format!("unknown morpheme `{}`", found);
397 let mut diag = Diagnostic::error(message, span).with_code("E0100");
398
399 let morphemes = [
401 ("τ", "tau", "transform/map"),
402 ("φ", "phi", "filter"),
403 ("σ", "sigma", "sort"),
404 ("ρ", "rho", "reduce"),
405 ("λ", "lambda", "anonymous function"),
406 ("Σ", "sum", "sum all"),
407 ("Π", "pi", "product"),
408 ("α", "alpha", "first element"),
409 ("ω", "omega", "last element"),
410 ("μ", "mu", "middle element"),
411 ("χ", "chi", "random choice"),
412 ("ν", "nu", "nth element"),
413 ("ξ", "xi", "next in sequence"),
414 ];
415
416 if let Some((greek, _ascii, desc)) = morphemes
417 .iter()
418 .find(|(g, a, _)| jaro_winkler(found, g) > 0.8 || jaro_winkler(found, a) > 0.8)
419 {
420 diag = diag.with_suggestion(
421 format!("did you mean the {} morpheme?", desc),
422 span,
423 greek.to_string(),
424 );
425 }
426
427 diag = diag.with_note(
428 "transform morphemes: τ (map), φ (filter), σ (sort), ρ (reduce), Σ (sum), Π (product)",
429 );
430 diag = diag.with_note(
431 "access morphemes: α (first), ω (last), μ (middle), χ (choice), ν (nth), ξ (next)",
432 );
433
434 diag
435 }
436
437 pub fn suggest_unicode_symbol(ascii: &str) -> Option<(&'static str, &'static str)> {
440 match ascii {
441 "&&" => Some(("∧", "logical AND")),
443 "||" => Some(("∨", "logical OR")),
444 "^^" => Some(("⊻", "logical XOR")),
445
446 "&" => Some(("⋏", "bitwise AND")),
448 "|" => Some(("⋎", "bitwise OR")),
449
450 "union" => Some(("∪", "set union")),
452 "intersect" | "intersection" => Some(("∩", "set intersection")),
453 "subset" => Some(("⊂", "proper subset")),
454 "superset" => Some(("⊃", "proper superset")),
455 "in" | "element_of" => Some(("∈", "element of")),
456 "not_in" => Some(("∉", "not element of")),
457
458 "sqrt" => Some(("√", "square root")),
460 "cbrt" => Some(("∛", "cube root")),
461 "infinity" | "inf" => Some(("∞", "infinity")),
462 "pi" => Some(("π", "pi constant")),
463 "sum" => Some(("Σ", "summation")),
464 "product" => Some(("Π", "product")),
465 "integral" => Some(("∫", "integral/cumulative sum")),
466 "partial" | "derivative" => Some(("∂", "partial/derivative")),
467
468 "tau" | "map" | "transform" => Some(("τ", "transform morpheme")),
470 "phi" | "filter" => Some(("φ", "filter morpheme")),
471 "sigma" | "sort" => Some(("σ", "sort morpheme")),
472 "rho" | "reduce" | "fold" => Some(("ρ", "reduce morpheme")),
473 "lambda" => Some(("λ", "lambda")),
474 "alpha" | "first" => Some(("α", "first element")),
475 "omega" | "last" => Some(("ω", "last element")),
476 "mu" | "middle" | "median" => Some(("μ", "middle element")),
477 "chi" | "choice" | "random" => Some(("χ", "random choice")),
478 "nu" | "nth" => Some(("ν", "nth element")),
479 "xi" | "next" => Some(("ξ", "next in sequence")),
480 "delta" | "diff" | "change" => Some(("δ", "delta/change")),
481 "epsilon" | "empty" => Some(("ε", "epsilon/empty")),
482 "zeta" | "zip" => Some(("ζ", "zeta/zip")),
483
484 "compose" => Some(("∘", "function composition")),
486 "tensor" => Some(("⊗", "tensor product")),
487 "direct_sum" | "xor" => Some(("⊕", "direct sum/XOR")),
488
489 "null" | "void" | "nothing" => Some(("∅", "empty set")),
491 "true" | "top" | "any" => Some(("⊤", "top/true")),
492 "false" | "bottom" | "never" => Some(("⊥", "bottom/false")),
493
494 "forall" | "for_all" => Some(("∀", "universal quantifier")),
496 "exists" => Some(("∃", "existential quantifier")),
497
498 "join" | "zip_with" => Some(("⋈", "join/zip with")),
500 "flatten" => Some(("⋳", "flatten")),
501 "max" | "supremum" => Some(("⊔", "supremum/max")),
502 "min" | "infimum" => Some(("⊓", "infimum/min")),
503
504 _ => None,
505 }
506 }
507
508 pub fn suggest_symbol_upgrade(ascii: &str, span: Span) -> Option<Diagnostic> {
510 Self::suggest_unicode_symbol(ascii).map(|(unicode, desc)| {
511 Diagnostic::warning(
512 format!("consider using Unicode symbol `{}` for {}", unicode, desc),
513 span,
514 )
515 .with_code("W0200")
516 .with_suggestion(
517 format!("use `{}` for clearer, more idiomatic Sigil", unicode),
518 span,
519 unicode.to_string(),
520 )
521 .with_note(format!(
522 "Sigil supports Unicode symbols. `{}` → `{}` ({})",
523 ascii, unicode, desc
524 ))
525 })
526 }
527
528 pub fn all_symbol_mappings() -> Vec<(&'static str, &'static str, &'static str)> {
530 vec![
531 ("&&", "∧", "logical AND"),
533 ("||", "∨", "logical OR"),
534 ("^^", "⊻", "logical XOR"),
535 ("&", "⋏", "bitwise AND"),
536 ("|", "⋎", "bitwise OR"),
537 ("union", "∪", "set union"),
538 ("intersect", "∩", "set intersection"),
539 ("subset", "⊂", "proper subset"),
540 ("superset", "⊃", "proper superset"),
541 ("in", "∈", "element of"),
542 ("not_in", "∉", "not element of"),
543 ("sqrt", "√", "square root"),
544 ("cbrt", "∛", "cube root"),
545 ("infinity", "∞", "infinity"),
546 ("tau", "τ", "transform"),
547 ("phi", "φ", "filter"),
548 ("sigma", "σ", "sort"),
549 ("rho", "ρ", "reduce"),
550 ("lambda", "λ", "lambda"),
551 ("alpha", "α", "first"),
552 ("omega", "ω", "last"),
553 ("mu", "μ", "middle"),
554 ("chi", "χ", "choice"),
555 ("nu", "ν", "nth"),
556 ("xi", "ξ", "next"),
557 ("sum", "Σ", "sum"),
558 ("product", "Π", "product"),
559 ("compose", "∘", "compose"),
560 ("tensor", "⊗", "tensor"),
561 ("xor", "⊕", "direct sum"),
562 ("forall", "∀", "for all"),
563 ("exists", "∃", "exists"),
564 ("null", "∅", "empty"),
565 ("true", "⊤", "top/true"),
566 ("false", "⊥", "bottom/false"),
567 ]
568 }
569
570 fn find_similar<'a>(name: &str, candidates: &[&'a str]) -> Option<&'a str> {
572 candidates
573 .iter()
574 .filter(|c| jaro_winkler(name, c) > 0.8)
575 .max_by(|a, b| {
576 jaro_winkler(name, a)
577 .partial_cmp(&jaro_winkler(name, b))
578 .unwrap_or(std::cmp::Ordering::Equal)
579 })
580 .copied()
581 }
582
583 fn suggest_token_fix(
585 expected: &str,
586 found: &Token,
587 _source: &str,
588 _span: Span,
589 ) -> Option<(String, String)> {
590 match (expected, found) {
592 ("`;`", Token::RBrace) => Some((
593 "you might be missing a semicolon".to_string(),
594 ";".to_string(),
595 )),
596 ("`{`", Token::Arrow) => Some((
597 "you might want a block here".to_string(),
598 "{ ... }".to_string(),
599 )),
600 ("`)`", Token::Comma) => Some((
601 "unexpected comma, maybe close the parenthesis first".to_string(),
602 ")".to_string(),
603 )),
604 _ => None,
605 }
606 }
607
608 fn evidentiality_help(expected: &str, found: &str) -> String {
610 match (expected, found) {
611 ("!", "~") => {
612 "use `value|validate!{...}` to promote reported data to known".to_string()
613 }
614 ("!", "?") => {
615 "handle the uncertain case with `match` or unwrap with `value!`".to_string()
616 }
617 ("?", "~") => "reported data is already uncertain, no conversion needed".to_string(),
618 _ => format!(
619 "evidentiality flows: ! (known) < ? (uncertain) < ~ (reported) < ‽ (paradox)"
620 ),
621 }
622 }
623}
624
625#[derive(Debug, Default)]
627pub struct Diagnostics {
628 items: Vec<Diagnostic>,
629 has_errors: bool,
630}
631
632impl Diagnostics {
633 pub fn new() -> Self {
634 Self::default()
635 }
636
637 pub fn add(&mut self, diagnostic: Diagnostic) {
638 if diagnostic.severity == Severity::Error {
639 self.has_errors = true;
640 }
641 self.items.push(diagnostic);
642 }
643
644 pub fn has_errors(&self) -> bool {
645 self.has_errors
646 }
647
648 pub fn is_empty(&self) -> bool {
649 self.items.is_empty()
650 }
651
652 pub fn iter(&self) -> impl Iterator<Item = &Diagnostic> {
653 self.items.iter()
654 }
655
656 pub fn render_all(&self, filename: &str, source: &str) -> String {
658 let mut output = String::new();
659 for diag in &self.items {
660 output.push_str(&diag.render(filename, source));
661 output.push('\n');
662 }
663 output
664 }
665
666 pub fn eprint_all(&self, filename: &str, source: &str) {
668 for diag in &self.items {
669 diag.eprint(filename, source);
670 }
671 }
672
673 pub fn error_count(&self) -> usize {
675 self.items
676 .iter()
677 .filter(|d| d.severity == Severity::Error)
678 .count()
679 }
680
681 pub fn warning_count(&self) -> usize {
683 self.items
684 .iter()
685 .filter(|d| d.severity == Severity::Warning)
686 .count()
687 }
688
689 pub fn print_summary(&self) {
691 let errors = self.error_count();
692 let warnings = self.warning_count();
693
694 if errors > 0 || warnings > 0 {
695 eprint!("\n");
696 if errors > 0 {
697 eprintln!(
698 "{}: aborting due to {} previous error{}",
699 "error".fg(Color::Red),
700 errors,
701 if errors == 1 { "" } else { "s" }
702 );
703 }
704 if warnings > 0 {
705 eprintln!(
706 "{}: {} warning{} emitted",
707 "warning".fg(Color::Yellow),
708 warnings,
709 if warnings == 1 { "" } else { "s" }
710 );
711 }
712 }
713 }
714
715 pub fn to_json_output(&self, filename: &str, source: &str) -> JsonDiagnosticsOutput {
720 let diagnostics: Vec<JsonDiagnostic> = self
721 .items
722 .iter()
723 .map(|d| d.to_json(filename, source))
724 .collect();
725
726 let error_count = self.error_count();
727 let warning_count = self.warning_count();
728
729 JsonDiagnosticsOutput {
730 file: filename.to_string(),
731 diagnostics,
732 error_count,
733 warning_count,
734 success: error_count == 0,
735 }
736 }
737
738 pub fn to_json_string(&self, filename: &str, source: &str) -> String {
742 let output = self.to_json_output(filename, source);
743 serde_json::to_string_pretty(&output).unwrap_or_else(|_| "{}".to_string())
744 }
745
746 pub fn to_json_compact(&self, filename: &str, source: &str) -> String {
750 let output = self.to_json_output(filename, source);
751 serde_json::to_string(&output).unwrap_or_else(|_| "{}".to_string())
752 }
753}
754
755#[cfg(test)]
756mod tests {
757 use super::*;
758
759 #[test]
760 fn test_undefined_variable_suggestion() {
761 let known = vec!["counter", "count", "total", "sum"];
762 let diag = DiagnosticBuilder::undefined_variable("countr", Span::new(10, 16), &known);
763
764 assert!(diag.suggestions.iter().any(|s| s.replacement == "counter"));
765 }
766
767 #[test]
768 fn test_evidentiality_mismatch() {
769 let diag = DiagnosticBuilder::evidentiality_mismatch("!", "~", Span::new(0, 5));
770
771 assert!(diag.notes.iter().any(|n| n.contains("validate")));
772 }
773
774 #[test]
775 fn test_unicode_symbol_suggestions() {
776 assert_eq!(
778 DiagnosticBuilder::suggest_unicode_symbol("&&"),
779 Some(("∧", "logical AND"))
780 );
781 assert_eq!(
782 DiagnosticBuilder::suggest_unicode_symbol("||"),
783 Some(("∨", "logical OR"))
784 );
785
786 assert_eq!(
788 DiagnosticBuilder::suggest_unicode_symbol("&"),
789 Some(("⋏", "bitwise AND"))
790 );
791 assert_eq!(
792 DiagnosticBuilder::suggest_unicode_symbol("|"),
793 Some(("⋎", "bitwise OR"))
794 );
795
796 assert_eq!(
798 DiagnosticBuilder::suggest_unicode_symbol("tau"),
799 Some(("τ", "transform morpheme"))
800 );
801 assert_eq!(
802 DiagnosticBuilder::suggest_unicode_symbol("filter"),
803 Some(("φ", "filter morpheme"))
804 );
805 assert_eq!(
806 DiagnosticBuilder::suggest_unicode_symbol("alpha"),
807 Some(("α", "first element"))
808 );
809
810 assert_eq!(
812 DiagnosticBuilder::suggest_unicode_symbol("sqrt"),
813 Some(("√", "square root"))
814 );
815 assert_eq!(
816 DiagnosticBuilder::suggest_unicode_symbol("infinity"),
817 Some(("∞", "infinity"))
818 );
819
820 assert_eq!(DiagnosticBuilder::suggest_unicode_symbol("foobar"), None);
822 }
823
824 #[test]
825 fn test_symbol_upgrade_diagnostic() {
826 let diag = DiagnosticBuilder::suggest_symbol_upgrade("&&", Span::new(0, 2));
827 assert!(diag.is_some());
828
829 let d = diag.unwrap();
830 assert!(d.suggestions.iter().any(|s| s.replacement == "∧"));
831 assert!(d.notes.iter().any(|n| n.contains("logical AND")));
832 }
833
834 #[test]
835 fn test_unknown_morpheme_with_access_morphemes() {
836 let diag = DiagnosticBuilder::unknown_morpheme("alph", Span::new(0, 4));
837
838 assert!(diag.suggestions.iter().any(|s| s.replacement == "α"));
840 assert!(diag.notes.iter().any(|n| n.contains("transform morphemes")));
842 assert!(diag.notes.iter().any(|n| n.contains("access morphemes")));
843 }
844
845 #[test]
846 fn test_all_symbol_mappings() {
847 let mappings = DiagnosticBuilder::all_symbol_mappings();
848 assert!(!mappings.is_empty());
849
850 assert!(mappings.iter().any(|(a, u, _)| *a == "&&" && *u == "∧"));
852 assert!(mappings.iter().any(|(a, u, _)| *a == "tau" && *u == "τ"));
853 assert!(mappings.iter().any(|(a, u, _)| *a == "sqrt" && *u == "√"));
854 }
855}