Skip to main content

openbim_step/
diagnostic.rs

1//! Source spans and diagnostics for ISO 10303-21 input.
2
3use std::error::Error;
4use std::fmt;
5
6/// A half-open byte range in the original source.
7#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
8pub struct Span {
9    /// Inclusive byte offset.
10    pub start: usize,
11    /// Exclusive byte offset.
12    pub end: usize,
13}
14
15impl Span {
16    /// Creates a half-open source span.
17    #[must_use]
18    pub const fn new(start: usize, end: usize) -> Self {
19        Self { start, end }
20    }
21
22    /// Returns the number of bytes covered by this span.
23    #[must_use]
24    pub const fn len(self) -> usize {
25        self.end.saturating_sub(self.start)
26    }
27
28    /// Returns whether this span is empty.
29    #[must_use]
30    pub const fn is_empty(self) -> bool {
31        self.start >= self.end
32    }
33}
34
35/// A value paired with its source span.
36#[derive(Debug, Clone, PartialEq)]
37pub struct Spanned<T> {
38    /// Parsed or tokenized value.
39    pub value: T,
40    /// Byte range from which the value came.
41    pub span: Span,
42}
43
44impl<T> Spanned<T> {
45    /// Pairs a value with a source span.
46    #[must_use]
47    pub const fn new(value: T, span: Span) -> Self {
48        Self { value, span }
49    }
50}
51
52/// Source text used to resolve byte spans to human-readable locations.
53#[derive(Debug, Clone, Copy)]
54pub struct Source<'a> {
55    name: &'a str,
56    bytes: &'a [u8],
57}
58
59impl<'a> Source<'a> {
60    /// Creates a named source view.
61    #[must_use]
62    pub const fn new(name: &'a str, bytes: &'a [u8]) -> Self {
63        Self { name, bytes }
64    }
65
66    /// Resolves the start of a span to a one-based line and column.
67    #[must_use]
68    pub fn location(self, span: Span) -> SourceLocation<'a> {
69        let offset = span.start.min(self.bytes.len());
70        let prefix = &self.bytes[..offset];
71        let line = prefix
72            .iter()
73            .fold(1, |line, byte| line + usize::from(*byte == b'\n'));
74        let line_start = prefix
75            .iter()
76            .rposition(|byte| *byte == b'\n')
77            .map_or(0, |position| position + 1);
78        let line_end = self.bytes[line_start..]
79            .iter()
80            .position(|byte| *byte == b'\n')
81            .map_or(self.bytes.len(), |position| line_start + position);
82        let column = String::from_utf8_lossy(&self.bytes[line_start..offset])
83            .chars()
84            .count()
85            + 1;
86        SourceLocation {
87            source_name: self.name,
88            line,
89            column,
90            line_text: String::from_utf8_lossy(&self.bytes[line_start..line_end]).into_owned(),
91            span,
92        }
93    }
94}
95
96/// A source span resolved to a display location.
97#[derive(Debug, Clone, PartialEq, Eq)]
98pub struct SourceLocation<'a> {
99    /// Name supplied to [`Source::new`].
100    pub source_name: &'a str,
101    /// One-based line number.
102    pub line: usize,
103    /// One-based Unicode scalar column.
104    pub column: usize,
105    /// Lossily decoded source line for rendering a diagnostic.
106    pub line_text: String,
107    /// Original byte span.
108    pub span: Span,
109}
110
111/// Failure while tokenizing or semantically parsing a physical file.
112#[derive(Debug, Clone, PartialEq, Eq)]
113pub struct StepError {
114    span: Span,
115    detail: String,
116    kind: ErrorKind,
117}
118
119#[derive(Debug, Clone, Copy, PartialEq, Eq)]
120enum ErrorKind {
121    NotStep,
122    Syntax,
123    RealWithoutPoint,
124    InvalidArgument,
125}
126
127impl StepError {
128    pub(crate) fn not_step(detail: impl Into<String>) -> Self {
129        Self {
130            span: Span::new(0, 0),
131            detail: detail.into(),
132            kind: ErrorKind::NotStep,
133        }
134    }
135
136    pub(crate) fn syntax(span: Span, detail: impl Into<String>) -> Self {
137        Self {
138            span,
139            detail: detail.into(),
140            kind: ErrorKind::Syntax,
141        }
142    }
143
144    pub(crate) fn real_without_point(span: Span, token: &str) -> Self {
145        Self {
146            span,
147            detail: format!("real {token} has no decimal point before its exponent"),
148            kind: ErrorKind::RealWithoutPoint,
149        }
150    }
151
152    pub(crate) fn invalid_argument(detail: impl Into<String>) -> Self {
153        Self {
154            span: Span::new(0, 0),
155            detail: detail.into(),
156            kind: ErrorKind::InvalidArgument,
157        }
158    }
159
160    /// Returns whether the input lacked the ISO 10303-21 physical-file marker.
161    #[must_use]
162    pub const fn is_not_step(&self) -> bool {
163        matches!(self.kind, ErrorKind::NotStep)
164    }
165
166    /// Returns whether the failure is a real written without the decimal
167    /// point ISO 10303-21 requires before its exponent, such as `1E-05`.
168    ///
169    /// [`Self::span`] covers exactly the number, and [`Self::detail`] quotes
170    /// it. [`ParseOptions::accept_real_without_point`](crate::ParseOptions::accept_real_without_point)
171    /// reads such numbers as reals instead.
172    #[must_use]
173    pub const fn is_real_without_point(&self) -> bool {
174        matches!(self.kind, ErrorKind::RealWithoutPoint)
175    }
176
177    /// Byte span associated with the failure.
178    #[must_use]
179    pub const fn span(&self) -> Span {
180        self.span
181    }
182
183    /// Diagnostic detail without the location prefix.
184    #[must_use]
185    pub fn detail(&self) -> &str {
186        &self.detail
187    }
188}
189
190impl fmt::Display for StepError {
191    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
192        match self.kind {
193            ErrorKind::NotStep => write!(formatter, "not a STEP physical file: {}", self.detail),
194            ErrorKind::Syntax | ErrorKind::RealWithoutPoint => write!(
195                formatter,
196                "STEP syntax error at bytes {}..{}: {}",
197                self.span.start, self.span.end, self.detail
198            ),
199            ErrorKind::InvalidArgument => write!(formatter, "invalid argument: {}", self.detail),
200        }
201    }
202}
203
204impl Error for StepError {}