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
755use crate::parser::ParseError;
760use crate::typeck::{TypeError, TypeErrorCode};
761
762impl From<ParseError> for Diagnostic {
763 fn from(err: ParseError) -> Self {
764 match err {
765 ParseError::UnexpectedToken {
766 expected,
767 found,
768 span,
769 } => {
770 let found_str = format_token(&found);
771 let message = format!("expected {}, found {}", expected, found_str);
772
773 let mut diag = Diagnostic::error(message, span)
774 .with_code("E0001")
775 .with_label(span, format!("expected {} here", expected));
776
777 diag = add_token_suggestions(diag, &expected, &found, span);
779 diag
780 }
781 ParseError::UnexpectedEof => {
782 Diagnostic::error("unexpected end of file", Span::new(0, 0))
783 .with_code("E0002")
784 .with_note("the file ended unexpectedly while parsing")
785 .with_note("check for unclosed braces, brackets, or parentheses")
786 }
787 ParseError::InvalidNumber(msg) => {
788 Diagnostic::error(format!("invalid number literal: {}", msg), Span::new(0, 0))
789 .with_code("E0003")
790 .with_note("number literals must be valid integers or floats")
791 }
792 ParseError::Custom(msg) => {
793 Diagnostic::error(msg, Span::new(0, 0)).with_code("E0004")
794 }
795 }
796 }
797}
798
799impl From<&TypeError> for Diagnostic {
800 fn from(err: &TypeError) -> Self {
801 let span = err.span.unwrap_or_default();
802 let mut diag = Diagnostic::error(&err.message, span).with_code(err.code.code());
803
804 match err.code {
806 TypeErrorCode::TypeMismatch => {
807 diag = diag.with_note("types must match for this operation");
808 }
809 TypeErrorCode::UndefinedName => {
810 diag = diag.with_note("check spelling or add an import/definition");
811 }
812 TypeErrorCode::BorrowError => {
813 diag = diag.with_note("a value can only be borrowed once mutably, or multiple times immutably");
814 }
815 TypeErrorCode::EvidentialityError => {
816 diag = diag.with_note("evidentiality markers track the source and certainty of values");
817 }
818 TypeErrorCode::NonBoolCondition => {
819 diag = diag.with_note("conditions must evaluate to `true` or `false`");
820 }
821 TypeErrorCode::HeterogeneousArray => {
822 diag = diag.with_note("all elements in an array must have the same type");
823 }
824 TypeErrorCode::InvalidIndex => {
825 diag = diag.with_note("use integers like `0`, `1`, `2` to index arrays");
826 }
827 TypeErrorCode::InvalidOperand => {
828 diag = diag.with_note("check that the operand types are valid for this operator");
829 }
830 TypeErrorCode::MissingMatchArm => {
831 diag = diag.with_note("add at least one arm to handle the matched value");
832 }
833 TypeErrorCode::InvalidReduction => {
834 diag = diag.with_note("reduction operations work on arrays or slices");
835 }
836 TypeErrorCode::Generic => {}
837 }
838
839 for note in &err.notes {
841 diag = diag.with_note(note);
842 }
843
844 diag
845 }
846}
847
848impl From<TypeError> for Diagnostic {
849 fn from(err: TypeError) -> Self {
850 Diagnostic::from(&err)
851 }
852}
853
854use crate::interpreter::{RuntimeError, RuntimeErrorCode};
855
856impl From<&RuntimeError> for Diagnostic {
857 fn from(err: &RuntimeError) -> Self {
858 let span = err.span.unwrap_or_default();
859 let mut diag = Diagnostic::error(&err.message, span).with_code(err.code.code());
860
861 match err.code {
863 RuntimeErrorCode::DivisionByZero => {
864 diag = diag.with_note("check that the divisor is not zero before dividing");
865 }
866 RuntimeErrorCode::IndexOutOfBounds => {
867 diag = diag.with_note("array indices must be between 0 and length - 1");
868 }
869 RuntimeErrorCode::UndefinedVariable => {
870 diag = diag.with_note("make sure the variable is defined before use");
871 }
872 RuntimeErrorCode::TypeError => {
873 diag = diag.with_note("values must have compatible types at runtime");
874 }
875 RuntimeErrorCode::InvalidOperation => {
876 diag = diag.with_note("this operation is not supported for this value");
877 }
878 RuntimeErrorCode::AssertionFailed => {
879 diag = diag.with_note("the assertion condition evaluated to false");
880 }
881 RuntimeErrorCode::Overflow => {
882 diag = diag.with_note("the result is too large to represent");
883 }
884 RuntimeErrorCode::StackOverflow => {
885 diag = diag.with_note("check for infinite recursion in your code");
886 }
887 RuntimeErrorCode::ControlFlowError => {
888 diag = diag.with_note("control flow statements must be used in the correct context");
889 }
890 RuntimeErrorCode::LinearTypeViolation => {
891 diag = diag.with_note("linear values can only be used once (no-cloning theorem)");
892 }
893 RuntimeErrorCode::Generic => {}
894 }
895
896 diag
897 }
898}
899
900impl From<RuntimeError> for Diagnostic {
901 fn from(err: RuntimeError) -> Self {
902 Diagnostic::from(&err)
903 }
904}
905
906fn format_token(token: &Token) -> String {
908 match token {
909 Token::Ident(s) => format!("identifier `{}`", s),
910 Token::IntLit(n) => format!("integer `{}`", n),
911 Token::FloatLit(f) => format!("float `{}`", f),
912 Token::StringLit(s) => format!("string {:?}", s),
913 Token::CharLit(c) => format!("character {:?}", c),
914 Token::LParen => "`(`".to_string(),
915 Token::RParen => "`)`".to_string(),
916 Token::LBrace => "`{`".to_string(),
917 Token::RBrace => "`}`".to_string(),
918 Token::LBracket => "`[`".to_string(),
919 Token::RBracket => "`]`".to_string(),
920 Token::Semi => "`;`".to_string(),
921 Token::Colon => "`:`".to_string(),
922 Token::ColonColon => "`::`".to_string(),
923 Token::Comma => "`,`".to_string(),
924 Token::Dot => "`.`".to_string(),
925 Token::DotDot => "`..`".to_string(),
926 Token::Arrow => "`->`".to_string(),
927 Token::FatArrow => "`=>`".to_string(),
928 Token::Eq => "`=`".to_string(),
929 Token::EqEq => "`==`".to_string(),
930 Token::NotEq => "`!=`".to_string(),
931 Token::Lt => "`<`".to_string(),
932 Token::LtEq => "`<=`".to_string(),
933 Token::Gt => "`>`".to_string(),
934 Token::GtEq => "`>=`".to_string(),
935 Token::Plus => "`+`".to_string(),
936 Token::Minus => "`-`".to_string(),
937 Token::Star => "`*`".to_string(),
938 Token::Slash => "`/`".to_string(),
939 Token::Percent => "`%`".to_string(),
940 Token::Amp => "`&`".to_string(),
941 Token::Pipe => "`|`".to_string(),
942 Token::AndAnd => "`&&`".to_string(),
943 Token::OrOr => "`||`".to_string(),
944 Token::Bang => "`!`".to_string(),
945 Token::Question => "`?`".to_string(),
946 Token::Tilde => "`~`".to_string(),
947 Token::Caret => "`^`".to_string(),
948 Token::Fn => "`fn`".to_string(),
949 Token::Let => "`let`".to_string(),
950 Token::Mut => "`mut`".to_string(),
951 Token::If => "`if`".to_string(),
952 Token::Else => "`else`".to_string(),
953 Token::While => "`while`".to_string(),
954 Token::For => "`for`".to_string(),
955 Token::In => "`in`".to_string(),
956 Token::Return => "`return`".to_string(),
957 Token::Break => "`break`".to_string(),
958 Token::Continue => "`continue`".to_string(),
959 Token::Struct => "`struct`".to_string(),
960 Token::Enum => "`enum`".to_string(),
961 Token::Impl => "`impl`".to_string(),
962 Token::Trait => "`trait`".to_string(),
963 Token::Pub => "`pub`".to_string(),
964 Token::Use => "`use`".to_string(),
965 Token::Mod => "`mod`".to_string(),
966 Token::True => "`true`".to_string(),
967 Token::False => "`false`".to_string(),
968 Token::Match => "`match`".to_string(),
969 Token::SelfLower => "`self`".to_string(),
970 Token::SelfUpper => "`Self`".to_string(),
971 Token::Const => "`const`".to_string(),
972 Token::Static => "`static`".to_string(),
973 Token::Type => "`type`".to_string(),
974 Token::Where => "`where`".to_string(),
975 Token::As => "`as`".to_string(),
976 Token::Async => "`async`".to_string(),
977 Token::Await => "`await`".to_string(),
978 Token::Unsafe => "`unsafe`".to_string(),
979 Token::Extern => "`extern`".to_string(),
980 Token::Crate => "`crate`".to_string(),
981 Token::Super => "`super`".to_string(),
982 Token::Dyn => "`dyn`".to_string(),
983 Token::Move => "`move`".to_string(),
984 Token::Ref => "`ref`".to_string(),
985 Token::Loop => "`loop`".to_string(),
986 _ => format!("`{:?}`", token),
987 }
988}
989
990fn add_token_suggestions(mut diag: Diagnostic, expected: &str, found: &Token, span: Span) -> Diagnostic {
992 if expected.contains(';') && matches!(found, Token::RBrace | Token::Fn | Token::Let | Token::Struct) {
994 diag = diag
995 .with_note("statements must end with a semicolon")
996 .with_suggestion("add a semicolon", span, ";");
997 }
998
999 if expected.contains("==") && matches!(found, Token::Eq) {
1001 diag = diag
1002 .with_note("use `==` for comparison, `=` is for assignment")
1003 .with_suggestion("use comparison operator", span, "==");
1004 }
1005
1006 if expected.contains('{') && matches!(found, Token::LParen) {
1008 diag = diag
1009 .with_note("blocks use curly braces `{}`")
1010 .with_suggestion("use curly brace", span, "{");
1011 }
1012
1013 if expected.contains(')') && matches!(found, Token::Semi | Token::RBrace) {
1015 diag = diag.with_note("you may have unclosed parentheses");
1016 }
1017 if expected.contains(']') && matches!(found, Token::Semi | Token::RBrace) {
1018 diag = diag.with_note("you may have unclosed brackets");
1019 }
1020 if expected.contains('}') && matches!(found, Token::RBracket | Token::RParen) {
1021 diag = diag.with_note("you may have unclosed braces");
1022 }
1023
1024 if expected.contains("=>") && matches!(found, Token::Arrow) {
1026 diag = diag
1027 .with_note("use `=>` for match arms, `->` is for return types")
1028 .with_suggestion("use fat arrow for match arm", span, "=>");
1029 }
1030
1031 if expected.contains("->") && matches!(found, Token::FatArrow) {
1033 diag = diag
1034 .with_note("use `->` for return types, `=>` is for match arms")
1035 .with_suggestion("use thin arrow for return type", span, "->");
1036 }
1037
1038 diag
1039}
1040
1041#[cfg(test)]
1042mod tests {
1043 use super::*;
1044
1045 #[test]
1046 fn test_undefined_variable_suggestion() {
1047 let known = vec!["counter", "count", "total", "sum"];
1048 let diag = DiagnosticBuilder::undefined_variable("countr", Span::new(10, 16), &known);
1049
1050 assert!(diag.suggestions.iter().any(|s| s.replacement == "counter"));
1051 }
1052
1053 #[test]
1054 fn test_evidentiality_mismatch() {
1055 let diag = DiagnosticBuilder::evidentiality_mismatch("!", "~", Span::new(0, 5));
1056
1057 assert!(diag.notes.iter().any(|n| n.contains("validate")));
1058 }
1059
1060 #[test]
1061 fn test_unicode_symbol_suggestions() {
1062 assert_eq!(
1064 DiagnosticBuilder::suggest_unicode_symbol("&&"),
1065 Some(("∧", "logical AND"))
1066 );
1067 assert_eq!(
1068 DiagnosticBuilder::suggest_unicode_symbol("||"),
1069 Some(("∨", "logical OR"))
1070 );
1071
1072 assert_eq!(
1074 DiagnosticBuilder::suggest_unicode_symbol("&"),
1075 Some(("⋏", "bitwise AND"))
1076 );
1077 assert_eq!(
1078 DiagnosticBuilder::suggest_unicode_symbol("|"),
1079 Some(("⋎", "bitwise OR"))
1080 );
1081
1082 assert_eq!(
1084 DiagnosticBuilder::suggest_unicode_symbol("tau"),
1085 Some(("τ", "transform morpheme"))
1086 );
1087 assert_eq!(
1088 DiagnosticBuilder::suggest_unicode_symbol("filter"),
1089 Some(("φ", "filter morpheme"))
1090 );
1091 assert_eq!(
1092 DiagnosticBuilder::suggest_unicode_symbol("alpha"),
1093 Some(("α", "first element"))
1094 );
1095
1096 assert_eq!(
1098 DiagnosticBuilder::suggest_unicode_symbol("sqrt"),
1099 Some(("√", "square root"))
1100 );
1101 assert_eq!(
1102 DiagnosticBuilder::suggest_unicode_symbol("infinity"),
1103 Some(("∞", "infinity"))
1104 );
1105
1106 assert_eq!(DiagnosticBuilder::suggest_unicode_symbol("foobar"), None);
1108 }
1109
1110 #[test]
1111 fn test_symbol_upgrade_diagnostic() {
1112 let diag = DiagnosticBuilder::suggest_symbol_upgrade("&&", Span::new(0, 2));
1113 assert!(diag.is_some());
1114
1115 let d = diag.unwrap();
1116 assert!(d.suggestions.iter().any(|s| s.replacement == "∧"));
1117 assert!(d.notes.iter().any(|n| n.contains("logical AND")));
1118 }
1119
1120 #[test]
1121 fn test_unknown_morpheme_with_access_morphemes() {
1122 let diag = DiagnosticBuilder::unknown_morpheme("alph", Span::new(0, 4));
1123
1124 assert!(diag.suggestions.iter().any(|s| s.replacement == "α"));
1126 assert!(diag.notes.iter().any(|n| n.contains("transform morphemes")));
1128 assert!(diag.notes.iter().any(|n| n.contains("access morphemes")));
1129 }
1130
1131 #[test]
1132 fn test_all_symbol_mappings() {
1133 let mappings = DiagnosticBuilder::all_symbol_mappings();
1134 assert!(!mappings.is_empty());
1135
1136 assert!(mappings.iter().any(|(a, u, _)| *a == "&&" && *u == "∧"));
1138 assert!(mappings.iter().any(|(a, u, _)| *a == "tau" && *u == "τ"));
1139 assert!(mappings.iter().any(|(a, u, _)| *a == "sqrt" && *u == "√"));
1140 }
1141}