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    InvalidArgument,
124}
125
126impl StepError {
127    pub(crate) fn not_step(detail: impl Into<String>) -> Self {
128        Self {
129            span: Span::new(0, 0),
130            detail: detail.into(),
131            kind: ErrorKind::NotStep,
132        }
133    }
134
135    pub(crate) fn syntax(span: Span, detail: impl Into<String>) -> Self {
136        Self {
137            span,
138            detail: detail.into(),
139            kind: ErrorKind::Syntax,
140        }
141    }
142
143    pub(crate) fn invalid_argument(detail: impl Into<String>) -> Self {
144        Self {
145            span: Span::new(0, 0),
146            detail: detail.into(),
147            kind: ErrorKind::InvalidArgument,
148        }
149    }
150
151    /// Returns whether the input lacked the ISO 10303-21 physical-file marker.
152    #[must_use]
153    pub const fn is_not_step(&self) -> bool {
154        matches!(self.kind, ErrorKind::NotStep)
155    }
156
157    /// Byte span associated with the failure.
158    #[must_use]
159    pub const fn span(&self) -> Span {
160        self.span
161    }
162
163    /// Diagnostic detail without the location prefix.
164    #[must_use]
165    pub fn detail(&self) -> &str {
166        &self.detail
167    }
168}
169
170impl fmt::Display for StepError {
171    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
172        match self.kind {
173            ErrorKind::NotStep => write!(formatter, "not a STEP physical file: {}", self.detail),
174            ErrorKind::Syntax => write!(
175                formatter,
176                "STEP syntax error at bytes {}..{}: {}",
177                self.span.start, self.span.end, self.detail
178            ),
179            ErrorKind::InvalidArgument => write!(formatter, "invalid argument: {}", self.detail),
180        }
181    }
182}
183
184impl Error for StepError {}