trident/diagnostic/
mod.rs1use crate::span::Span;
2use std::cell::Cell;
3
4thread_local! {
5 static SUPPRESS_WARNINGS: Cell<bool> = const { Cell::new(false) };
6}
7
8pub fn suppress_warnings() -> SuppressGuard {
11 let prev = SUPPRESS_WARNINGS.with(|s| s.replace(true));
12 SuppressGuard(prev)
13}
14
15pub struct SuppressGuard(bool);
16
17impl Drop for SuppressGuard {
18 fn drop(&mut self) {
19 SUPPRESS_WARNINGS.with(|s| s.set(self.0));
20 }
21}
22
23fn warnings_suppressed() -> bool {
24 SUPPRESS_WARNINGS.with(|s| s.get())
25}
26
27#[derive(Clone, Debug)]
29pub struct Diagnostic {
30 pub severity: Severity,
31 pub message: String,
32 pub span: Span,
33 pub notes: Vec<String>,
34 pub help: Option<String>,
35}
36
37#[derive(Clone, Copy, Debug, PartialEq, Eq)]
38pub enum Severity {
39 Error,
40 Warning,
41}
42
43impl Diagnostic {
44 pub fn error(message: String, span: Span) -> Self {
45 Self {
46 severity: Severity::Error,
47 message,
48 span,
49 notes: Vec::new(),
50 help: None,
51 }
52 }
53
54 pub fn warning(message: String, span: Span) -> Self {
55 Self {
56 severity: Severity::Warning,
57 message,
58 span,
59 notes: Vec::new(),
60 help: None,
61 }
62 }
63
64 pub fn with_note(mut self, note: String) -> Self {
65 self.notes.push(note);
66 self
67 }
68
69 pub fn with_help(mut self, help: String) -> Self {
70 self.help = Some(help);
71 self
72 }
73
74 pub fn render(&self, filename: &str, source: &str) {
76 if self.severity == Severity::Warning && warnings_suppressed() {
77 return;
78 }
79 use ariadne::{Color, Label, Report, ReportKind, Source};
80
81 let kind = match self.severity {
82 Severity::Error => ReportKind::Error,
83 Severity::Warning => ReportKind::Warning,
84 };
85
86 let color = match self.severity {
87 Severity::Error => Color::Red,
88 Severity::Warning => Color::Yellow,
89 };
90
91 let mut report = Report::build(kind, filename, self.span.start as usize)
92 .with_message(&self.message)
93 .with_label(
94 Label::new((filename, self.span.start as usize..self.span.end as usize))
95 .with_message(&self.message)
96 .with_color(color),
97 );
98
99 for note in &self.notes {
100 report = report.with_note(note);
101 }
102
103 if let Some(help) = &self.help {
104 report = report.with_help(help);
105 }
106
107 let _ = report.finish().eprint((filename, Source::from(source)));
108 }
109}
110
111pub fn render_diagnostics(diagnostics: &[Diagnostic], filename: &str, source: &str) {
113 for diag in diagnostics {
114 diag.render(filename, source);
115 }
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121
122 #[test]
123 fn test_error_construction() {
124 let span = Span::new(0, 10, 15);
125 let d = Diagnostic::error("type mismatch".to_string(), span);
126 assert_eq!(d.severity, Severity::Error);
127 assert_eq!(d.message, "type mismatch");
128 assert_eq!(d.span.start, 10);
129 assert_eq!(d.span.end, 15);
130 assert!(d.notes.is_empty());
131 assert!(d.help.is_none());
132 }
133
134 #[test]
135 fn test_warning_construction() {
136 let span = Span::dummy();
137 let d = Diagnostic::warning("unused variable".to_string(), span);
138 assert_eq!(d.severity, Severity::Warning);
139 assert_eq!(d.message, "unused variable");
140 }
141
142 #[test]
143 fn test_with_note() {
144 let d = Diagnostic::error("error".to_string(), Span::dummy())
145 .with_note("expected Field".to_string())
146 .with_note("found U32".to_string());
147 assert_eq!(d.notes.len(), 2);
148 assert_eq!(d.notes[0], "expected Field");
149 assert_eq!(d.notes[1], "found U32");
150 }
151
152 #[test]
153 fn test_with_help() {
154 let d = Diagnostic::error("error".to_string(), Span::dummy())
155 .with_help("try as_field()".to_string());
156 assert_eq!(d.help.as_deref(), Some("try as_field()"));
157 }
158
159 #[test]
160 fn test_chained_builders() {
161 let d = Diagnostic::warning("hint".to_string(), Span::new(0, 0, 5))
162 .with_note("note 1".to_string())
163 .with_help("help text".to_string())
164 .with_note("note 2".to_string());
165 assert_eq!(d.severity, Severity::Warning);
166 assert_eq!(d.notes.len(), 2);
167 assert!(d.help.is_some());
168 }
169
170 #[test]
171 fn test_render_does_not_panic() {
172 let source = "let x: Field = 1\nlet y: U32 = x\n";
173 let d = Diagnostic::error("type mismatch".to_string(), Span::new(0, 18, 32))
174 .with_note("expected U32, found Field".to_string());
175 d.render("test.tri", source);
177 }
178
179 #[test]
180 fn test_render_diagnostics_multiple() {
181 let source = "let x = 1\nlet y = 2\n";
182 let diagnostics = vec![
183 Diagnostic::warning("unused x".to_string(), Span::new(0, 4, 5)),
184 Diagnostic::warning("unused y".to_string(), Span::new(0, 14, 15)),
185 ];
186 render_diagnostics(&diagnostics, "test.tri", source);
188 }
189
190 #[test]
191 fn test_render_warning_does_not_panic() {
192 let source = "fn main() {\n as_u32(x)\n}\n";
193 let d = Diagnostic::warning("redundant range check".to_string(), Span::new(0, 16, 25))
194 .with_help("x is already proven U32".to_string());
195 d.render("test.tri", source);
196 }
197}