synkit_core/traits/stream.rs
1use super::parse::Parse;
2use super::peek::Peek;
3use crate::Error;
4
5/// A span representing a source location range.
6///
7/// Spans track the byte offsets of tokens and AST nodes within source text.
8/// All implementations must be `Clone` to support parser backtracking.
9pub trait SpanLike: Clone {
10 /// Returns the start byte offset.
11 fn start(&self) -> usize;
12
13 /// Returns the end byte offset (exclusive).
14 fn end(&self) -> usize;
15
16 /// Creates a new span from start and end offsets.
17 fn new(start: usize, end: usize) -> Self;
18
19 /// Returns a synthetic span for generated code.
20 fn call_site() -> Self;
21
22 /// Returns the length of this span.
23 ///
24 /// # Clamping Behavior
25 ///
26 /// Uses saturating subtraction to compute `end - start`. If `end < start`
27 /// (an inverted span), this returns `0` rather than panicking or wrapping.
28 /// This ensures safe handling of malformed or sentinel span values.
29 #[inline]
30 fn len(&self) -> usize {
31 self.end().saturating_sub(self.start())
32 }
33
34 #[inline]
35 fn is_empty(&self) -> bool {
36 self.len() == 0
37 }
38
39 /// Join two spans into one covering both regions.
40 ///
41 /// # Clamping Behavior
42 ///
43 /// Uses `min()` for start and `max()` for end positions. No overflow checking
44 /// is performed since `min`/`max` operations cannot overflow. The result
45 /// spans from the earliest start to the latest end, regardless of whether
46 /// the input spans are inverted or disjoint.
47 #[inline]
48 fn join(&self, other: &Self) -> Self {
49 Self::new(self.start().min(other.start()), self.end().max(other.end()))
50 }
51}
52
53/// A value paired with its source location span.
54///
55/// Wraps any value `T` with span information for error reporting and
56/// source mapping. Implementations must be `Clone` to support backtracking.
57pub trait SpannedLike<T>: Clone {
58 /// The span type used to track source locations.
59 type Span: SpanLike;
60
61 /// Returns a reference to the span.
62 fn span(&self) -> &Self::Span;
63
64 /// Returns a reference to the wrapped value.
65 fn value_ref(&self) -> &T;
66
67 /// Consumes self and returns the wrapped value.
68 fn value(self) -> T;
69
70 /// Creates a new spanned value from offsets and a value.
71 fn new(start: usize, end: usize, value: T) -> Self;
72
73 /// Maps the inner value while preserving the span.
74 #[inline]
75 fn map<U: Clone, F: FnOnce(T) -> U>(self, f: F) -> impl SpannedLike<U, Span = Self::Span>
76 where
77 Self: Sized,
78 {
79 let span = self.span().clone();
80 MappedSpanned {
81 span,
82 value: f(self.value()),
83 }
84 }
85}
86
87#[derive(Clone)]
88struct MappedSpanned<T, S> {
89 span: S,
90 value: T,
91}
92
93impl<T: Clone, S: SpanLike> SpannedLike<T> for MappedSpanned<T, S> {
94 type Span = S;
95
96 #[inline]
97 fn span(&self) -> &Self::Span {
98 &self.span
99 }
100
101 #[inline]
102 fn value_ref(&self) -> &T {
103 &self.value
104 }
105
106 #[inline]
107 fn value(self) -> T {
108 self.value
109 }
110
111 #[inline]
112 fn new(start: usize, end: usize, value: T) -> Self {
113 Self {
114 span: S::new(start, end),
115 value,
116 }
117 }
118}
119
120/// A stream of tokens for parsing.
121///
122/// Provides the core interface for lexer output consumption. Token streams
123/// support peeking, consumption, forking for lookahead, and rewinding.
124pub trait TokenStream: Sized {
125 /// The token type produced by the lexer.
126 type Token: Clone;
127
128 /// The span type for tracking source locations.
129 type Span: SpanLike;
130
131 /// A spanned wrapper type for associating values with spans.
132 type Spanned<T: Clone>: SpannedLike<T, Span = Self::Span>;
133
134 /// Peeks at the next token without consuming (includes whitespace).
135 fn peek_token_raw(&self) -> Option<&Self::Spanned<Self::Token>>;
136
137 /// Consumes and returns the next token (includes whitespace).
138 fn next_raw(&mut self) -> Option<Self::Spanned<Self::Token>>;
139
140 /// Returns the current cursor position.
141 fn cursor(&self) -> usize;
142
143 /// Rewinds to a previous cursor position.
144 fn rewind(&mut self, pos: usize);
145
146 /// Creates a fork for lookahead without consuming tokens.
147 fn fork(&self) -> Self;
148
149 /// Returns the span at the current cursor position.
150 fn cursor_span(&self) -> Option<Self::Span>;
151
152 /// Returns the span of the last consumed token.
153 fn last_span(&self) -> Option<Self::Span>;
154
155 /// Peeks at the next significant token (skips whitespace by default).
156 #[inline]
157 fn peek_token(&self) -> Option<&Self::Spanned<Self::Token>> {
158 self.peek_token_raw()
159 }
160
161 /// Consumes and returns the next significant token.
162 #[inline]
163 fn next(&mut self) -> Option<Self::Spanned<Self::Token>> {
164 self.next_raw()
165 }
166
167 /// Checks if the next token matches type `T` without consuming.
168 #[inline]
169 fn peek<T: Peek<Token = Self::Token>>(&self) -> bool {
170 T::peek(self)
171 }
172
173 /// Parses a value of type `T` from the stream.
174 #[inline]
175 fn parse<T: Parse<Token = Self::Token>>(&mut self) -> Result<T, T::Error> {
176 T::parse(self)
177 }
178
179 /// Parses a value and wraps it with its source span.
180 fn parse_spanned<T: Parse<Token = Self::Token> + Clone>(
181 &mut self,
182 ) -> Result<Self::Spanned<T>, T::Error> {
183 let start = self.cursor_span().unwrap_or_else(Self::Span::call_site);
184 let value = T::parse(self)?;
185 let end = self.last_span().unwrap_or_else(Self::Span::call_site);
186 Ok(Self::Spanned::new(start.start(), end.end(), value))
187 }
188
189 #[inline]
190 fn is_empty(&self) -> bool {
191 self.peek_token().is_none()
192 }
193
194 /// Returns the number of remaining significant tokens (excluding whitespace).
195 ///
196 /// The default implementation counts tokens via `peek_token()` and `next()`,
197 /// which may be inefficient. Implementations may override this for better performance.
198 fn remaining(&self) -> usize {
199 let mut count = 0;
200 let mut fork = self.fork();
201 while fork.next().is_some() {
202 count += 1;
203 }
204 count
205 }
206
207 /// Ensures the stream has been fully consumed.
208 ///
209 /// Returns `Ok(())` if no significant tokens remain (whitespace is ignored).
210 /// Returns `Err(Error::StreamNotConsumed)` if tokens remain.
211 ///
212 /// # Example
213 ///
214 /// ```ignore
215 /// let doc: Document = stream.parse()?;
216 /// stream.ensure_consumed()?; // Error if trailing garbage
217 /// ```
218 #[inline]
219 fn ensure_consumed(&self) -> Result<(), Error> {
220 let remaining = self.remaining();
221 if remaining > 0 {
222 Err(Error::StreamNotConsumed { remaining })
223 } else {
224 Ok(())
225 }
226 }
227
228 /// Create a span covering a range of cursor positions.
229 ///
230 /// This is useful for tracking the span of a parsed AST node that
231 /// spans multiple tokens.
232 ///
233 /// # Example
234 ///
235 /// ```ignore
236 /// let start = stream.cursor();
237 /// // ... parse multiple tokens ...
238 /// let end = stream.cursor();
239 /// let span = stream.span_range(start..end);
240 /// ```
241 #[inline]
242 fn span_range(&self, range: core::ops::Range<usize>) -> Self::Span {
243 Self::Span::new(range.start, range.end)
244 }
245
246 /// Get the span of a token at a specific cursor position.
247 ///
248 /// Returns `None` if the position is out of bounds.
249 fn span_at(&self, pos: usize) -> Option<Self::Span>;
250}