Skip to main content

rpic_core/
diagnostic.rs

1//! Structured compiler diagnostics for editor/front-end integrations.
2
3use std::sync::Arc;
4
5/// A source span in 1-based line/column coordinates.
6#[derive(Debug, Clone, PartialEq, Eq)]
7pub struct Span {
8    pub line: u32,
9    pub col: u32,
10    /// Exclusive end column when the diagnostic is on a single line.
11    pub end_col: u32,
12    /// Which source the position refers to: `None` is the user's own input;
13    /// `Some` names a `copy` include (as written in the source) or a loaded
14    /// library (`"circuits"`). Positions are always relative to their own
15    /// source, never to a concatenated stream.
16    pub file: Option<Arc<str>>,
17}
18
19impl Span {
20    pub fn new(line: u32, col: u32, end_col: u32) -> Self {
21        Self {
22            line,
23            col,
24            end_col,
25            file: None,
26        }
27    }
28
29    pub fn in_file(mut self, file: Option<Arc<str>>) -> Self {
30        self.file = file;
31        self
32    }
33}
34
35/// A structured diagnostic that can be consumed by live editors.
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct Diagnostic {
38    pub message: String,
39    pub line: Option<u32>,
40    pub col: Option<u32>,
41    pub end_col: Option<u32>,
42    /// See [`Span::file`]: `None` = the user's input, `Some` = an include or
43    /// library name the position is relative to.
44    pub file: Option<Arc<str>>,
45    pub kind: String,
46    pub found: Option<String>,
47    pub expected: Option<String>,
48    pub hint: Option<String>,
49}
50
51impl Diagnostic {
52    pub fn new(kind: impl Into<String>, message: impl Into<String>) -> Self {
53        Self {
54            message: message.into(),
55            line: None,
56            col: None,
57            end_col: None,
58            file: None,
59            kind: kind.into(),
60            found: None,
61            expected: None,
62            hint: None,
63        }
64    }
65
66    pub fn at(mut self, span: Span) -> Self {
67        self.line = Some(span.line);
68        self.col = Some(span.col);
69        self.end_col = Some(span.end_col);
70        self.file = span.file;
71        self
72    }
73
74    pub fn expected(mut self, expected: impl Into<String>) -> Self {
75        self.expected = Some(expected.into());
76        self
77    }
78
79    pub fn found(mut self, found: impl Into<String>) -> Self {
80        self.found = Some(found.into());
81        self
82    }
83
84    pub fn hint(mut self, hint: impl Into<String>) -> Self {
85        self.hint = Some(hint.into());
86        self
87    }
88}
89
90/// A failed compilation: the flat human-readable message (what the `Err(String)`
91/// entry points return) plus the structured [`Diagnostic`] behind it, for
92/// bindings that surface rich errors (editor integrations, exceptions with
93/// attached position data).
94#[derive(Debug, Clone, PartialEq, Eq)]
95pub struct CompileError {
96    pub message: String,
97    /// Boxed to keep `Result<_, CompileError>` small (clippy result_large_err).
98    pub info: Box<Diagnostic>,
99}
100
101impl std::fmt::Display for CompileError {
102    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
103        f.write_str(&self.message)
104    }
105}
106
107impl std::error::Error for CompileError {}
108
109/// The candidate closest to `word` within edit distance 2 (ties broken
110/// alphabetically), for "did you mean …?" hints.
111pub(crate) fn closest(word: &str, candidates: &'static [&'static str]) -> Option<&'static str> {
112    candidates
113        .iter()
114        .copied()
115        .filter_map(|candidate| {
116            let dist = edit_distance(word, candidate);
117            (dist <= 2).then_some((dist, candidate))
118        })
119        .min_by_key(|(dist, candidate)| (*dist, *candidate))
120        .map(|(_, candidate)| candidate)
121}
122
123fn edit_distance(a: &str, b: &str) -> usize {
124    let a: Vec<char> = a.chars().collect();
125    let b: Vec<char> = b.chars().collect();
126    let mut prev: Vec<usize> = (0..=b.len()).collect();
127    let mut cur = vec![0; b.len() + 1];
128    for (i, ca) in a.iter().enumerate() {
129        cur[0] = i + 1;
130        for (j, cb) in b.iter().enumerate() {
131            cur[j + 1] = if ca == cb {
132                prev[j]
133            } else {
134                1 + prev[j].min(prev[j + 1]).min(cur[j])
135            };
136        }
137        std::mem::swap(&mut prev, &mut cur);
138    }
139    prev[b.len()]
140}