rune/
source.rs

1//! Module for dealing with sources.
2//!
3//! The primary type in here is the [`Source`] struct, which holds onto all
4//! metadata necessary related to a source in order to build it.
5//!
6//! Sources are stored in the [`Sources`] collection.
7//!
8//! [`Sources`]: crate::sources::Sources
9
10#[cfg(feature = "emit")]
11use core::cmp;
12use core::fmt;
13use core::iter;
14#[cfg(feature = "emit")]
15use core::ops::Range;
16use core::slice;
17
18#[cfg(feature = "emit")]
19use std::io;
20#[cfg(feature = "std")]
21use std::path::Path;
22
23use crate as rune;
24#[cfg(feature = "std")]
25use crate::alloc::borrow::Cow;
26use crate::alloc::prelude::*;
27use crate::alloc::{self, Box};
28use crate::ast::Span;
29#[cfg(feature = "emit")]
30use crate::termcolor::{self, WriteColor};
31
32/// Error raised when constructing a source.
33#[derive(Debug)]
34pub struct FromPathError {
35    kind: FromPathErrorKind,
36}
37
38impl From<alloc::Error> for FromPathError {
39    fn from(error: alloc::Error) -> Self {
40        Self {
41            kind: FromPathErrorKind::Alloc(error),
42        }
43    }
44}
45
46#[cfg(feature = "std")]
47impl From<std::io::Error> for FromPathError {
48    fn from(error: std::io::Error) -> Self {
49        Self {
50            kind: FromPathErrorKind::Io(error),
51        }
52    }
53}
54
55impl fmt::Display for FromPathError {
56    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57        match &self.kind {
58            FromPathErrorKind::Alloc(error) => error.fmt(f),
59            #[cfg(feature = "std")]
60            FromPathErrorKind::Io(error) => error.fmt(f),
61        }
62    }
63}
64
65#[derive(Debug)]
66enum FromPathErrorKind {
67    Alloc(alloc::Error),
68    #[cfg(feature = "std")]
69    Io(std::io::Error),
70}
71
72impl core::error::Error for FromPathError {
73    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
74        match &self.kind {
75            FromPathErrorKind::Alloc(error) => Some(error),
76            #[cfg(feature = "std")]
77            FromPathErrorKind::Io(error) => Some(error),
78        }
79    }
80}
81
82/// A single source file.
83#[derive(Default, TryClone)]
84pub struct Source {
85    /// The name of the source.
86    name: SourceName,
87    /// The source string.
88    source: Box<str>,
89    /// The path the source was loaded from.
90    #[cfg(feature = "std")]
91    path: Option<Box<Path>>,
92    /// The starting byte indices in the source code.
93    line_starts: Box<[usize]>,
94}
95
96impl Source {
97    /// Construct a new source with the given name.
98    pub fn new(name: impl AsRef<str>, source: impl AsRef<str>) -> alloc::Result<Self> {
99        let name = Box::try_from(name.as_ref())?;
100        let source = source.as_ref();
101        let line_starts = line_starts(source).try_collect::<Box<[_]>>()?;
102
103        Ok(Self {
104            name: SourceName::Name(name),
105            source: source.try_into()?,
106            #[cfg(feature = "std")]
107            path: None,
108            line_starts,
109        })
110    }
111
112    /// Construct a new anonymously named `<memory>` source.
113    ///
114    /// # Examples
115    ///
116    /// ```
117    /// use rune::Source;
118    ///
119    /// let source = Source::memory("pub fn main() { 42 }")?;
120    /// assert_eq!(source.name(), "<memory>");
121    /// # Ok::<_, rune::support::Error>(())
122    /// ```
123    pub fn memory(source: impl AsRef<str>) -> alloc::Result<Self> {
124        let source = source.as_ref();
125        let line_starts = line_starts(source).try_collect::<Box<[_]>>()?;
126
127        Ok(Self {
128            name: SourceName::Memory,
129            source: source.try_into()?,
130            #[cfg(feature = "std")]
131            path: None,
132            line_starts,
133        })
134    }
135
136    /// Read and load a source from the given filesystem path.
137    #[cfg(feature = "std")]
138    #[cfg_attr(rune_docsrs, doc(cfg(feature = "std")))]
139    pub fn from_path(path: impl AsRef<Path>) -> Result<Self, FromPathError> {
140        let name = Box::try_from(Cow::try_from(path.as_ref().to_string_lossy())?)?;
141        let source = Box::try_from(std::fs::read_to_string(path.as_ref())?)?;
142        let path = Some(path.as_ref().try_into()?);
143        let line_starts = line_starts(source.as_ref()).try_collect::<Box<[_]>>()?;
144
145        Ok(Self {
146            name: SourceName::Name(name),
147            source,
148            path,
149            line_starts,
150        })
151    }
152
153    /// Construct a new source with the given content and path.
154    ///
155    /// # Examples
156    ///
157    /// ```
158    /// use std::path::Path;
159    /// use rune::Source;
160    ///
161    /// let source = Source::with_path("test", "pub fn main() { 42 }", "test.rn")?;
162    /// assert_eq!(source.name(), "test");
163    /// assert_eq!(source.path(), Some(Path::new("test.rn")));
164    /// # Ok::<_, rune::support::Error>(())
165    /// ```
166    #[cfg(feature = "std")]
167    #[cfg_attr(rune_docsrs, doc(cfg(feature = "std")))]
168    pub fn with_path(
169        name: impl AsRef<str>,
170        source: impl AsRef<str>,
171        path: impl AsRef<Path>,
172    ) -> alloc::Result<Self> {
173        let name = Box::try_from(name.as_ref())?;
174        let source = Box::try_from(source.as_ref())?;
175        let path = Some(path.as_ref().try_into()?);
176        let line_starts = line_starts(source.as_ref()).try_collect::<Box<[_]>>()?;
177
178        Ok(Self {
179            name: SourceName::Name(name),
180            source,
181            path,
182            line_starts,
183        })
184    }
185
186    /// Access all line starts in the source.
187    pub(crate) fn line_starts(&self) -> &[usize] {
188        &self.line_starts
189    }
190
191    /// Get the name of the source.
192    pub fn name(&self) -> &str {
193        match &self.name {
194            SourceName::Memory => "<memory>",
195            SourceName::Name(name) => name,
196        }
197    }
198
199    ///  et the given range from the source.
200    pub(crate) fn get<I>(&self, i: I) -> Option<&I::Output>
201    where
202        I: slice::SliceIndex<str>,
203    {
204        self.source.get(i)
205    }
206
207    /// Access the underlying string for the source.
208    pub(crate) fn as_str(&self) -> &str {
209        &self.source
210    }
211
212    /// Get the path associated with the source.
213    ///
214    /// # Examples
215    ///
216    /// ```
217    /// use std::path::Path;
218    /// use rune::Source;
219    ///
220    /// let source = Source::with_path("test", "pub fn main() { 42 }", "test.rn")?;
221    /// assert_eq!(source.name(), "test");
222    /// assert_eq!(source.path(), Some(Path::new("test.rn")));
223    /// # Ok::<_, rune::support::Error>(())
224    /// ```
225    #[cfg(feature = "std")]
226    #[cfg_attr(rune_docsrs, doc(cfg(feature = "std")))]
227    pub fn path(&self) -> Option<&Path> {
228        self.path.as_deref()
229    }
230
231    /// Convert the given position to a utf-8 line position in code units.
232    ///
233    /// A position is a character offset into the source in utf-8 characters.
234    ///
235    /// Note that utf-8 code units is what you'd count when using the
236    /// [`str::chars()`] iterator.
237    pub fn find_line_column(&self, position: usize) -> (usize, usize) {
238        let (line, offset, rest) = self.position(position);
239        let col = rest.char_indices().take_while(|&(n, _)| n < offset).count();
240        (line, col)
241    }
242
243    /// Convert the given position to a utf-16 code units line and character.
244    ///
245    /// A position is a character offset into the source in utf-16 characters.
246    ///
247    /// Note that utf-16 code units is what you'd count when iterating over the
248    /// string in terms of characters as-if they would have been encoded with
249    /// [`char::encode_utf16()`].
250    pub fn find_utf16cu_line_column(&self, position: usize) -> (usize, usize) {
251        let (line, offset, rest) = self.position(position);
252
253        let col = rest
254            .char_indices()
255            .flat_map(|(n, c)| (n < offset).then(|| c.encode_utf16(&mut [0u16; 2]).len()))
256            .sum();
257
258        (line, col)
259    }
260
261    /// Fetch [`SourceLine`] information for the given span.
262    pub fn source_line(&self, span: Span) -> Option<SourceLine<'_>> {
263        let (line, column, text, _span) = line_for(self, span)?;
264
265        Some(SourceLine {
266            #[cfg(feature = "emit")]
267            name: self.name(),
268            line,
269            column,
270            text,
271            #[cfg(feature = "emit")]
272            span: _span,
273        })
274    }
275
276    /// Get the line index for the given byte.
277    #[cfg(feature = "emit")]
278    pub(crate) fn line_index(&self, byte_index: usize) -> usize {
279        self.line_starts
280            .binary_search(&byte_index)
281            .unwrap_or_else(|next_line| next_line.saturating_sub(1))
282    }
283
284    /// Get the range corresponding to the given line index.
285    #[cfg(feature = "emit")]
286    pub(crate) fn line_range(&self, line_index: usize) -> Option<Range<usize>> {
287        let line_start = self.line_start(line_index)?;
288        let next_line_start = self.line_start(line_index.saturating_add(1))?;
289        Some(line_start..next_line_start)
290    }
291
292    /// Get the number of lines in the source.
293    #[cfg(feature = "emit")]
294    pub(crate) fn line_count(&self) -> usize {
295        self.line_starts.len()
296    }
297
298    /// Access the line number of content that starts with the given span.
299    #[cfg(feature = "emit")]
300    pub(crate) fn line(&self, span: Span) -> Option<(usize, usize, [&str; 3])> {
301        let from = span.range();
302        let (lin, col) = self.find_line_column(from.start);
303        let line = self.line_range(lin)?;
304
305        let start = from.start.checked_sub(line.start)?;
306        let end = from.end.checked_sub(line.start)?;
307
308        let text = self.source.get(line)?;
309        let prefix = text.get(..start)?;
310        let mid = text.get(start..end)?;
311        let suffix = text.get(end..)?;
312
313        Some((lin, col, [prefix, mid, suffix]))
314    }
315
316    fn position(&self, offset: usize) -> (usize, usize, &str) {
317        if offset == 0 {
318            return Default::default();
319        }
320
321        let line = match self.line_starts.binary_search(&offset) {
322            Ok(exact) => exact,
323            Err(0) => return Default::default(),
324            Err(n) => n - 1,
325        };
326
327        let line_start = self.line_starts[line];
328
329        let rest = &self.source[line_start..];
330        let offset = offset.saturating_sub(line_start);
331        (line, offset, rest)
332    }
333
334    #[cfg(feature = "emit")]
335    fn line_start(&self, line_index: usize) -> Option<usize> {
336        match line_index.cmp(&self.line_starts.len()) {
337            cmp::Ordering::Less => self.line_starts.get(line_index).copied(),
338            cmp::Ordering::Equal => Some(self.source.as_ref().len()),
339            cmp::Ordering::Greater => None,
340        }
341    }
342
343    pub(crate) fn len(&self) -> usize {
344        self.source.len()
345    }
346}
347
348impl fmt::Debug for Source {
349    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
350        let mut st = f.debug_struct("Source");
351        st.field("name", &self.name);
352        #[cfg(feature = "std")]
353        st.field("path", &self.path);
354        st.finish()
355    }
356}
357
358/// An extracted source line.
359pub struct SourceLine<'a> {
360    #[cfg(feature = "emit")]
361    name: &'a str,
362    /// The line number in the source.
363    pub line: usize,
364    /// The column number in the source.
365    pub column: usize,
366    /// The text of the span.
367    pub text: &'a str,
368    #[cfg(feature = "emit")]
369    span: Span,
370}
371
372impl SourceLine<'_> {
373    /// Pretty write a source line to the given output.
374    #[cfg(feature = "emit")]
375    pub(crate) fn write(&self, o: &mut dyn WriteColor) -> io::Result<()> {
376        let mut highlight = termcolor::ColorSpec::new();
377        highlight.set_fg(Some(termcolor::Color::Yellow));
378
379        let mut new_line = termcolor::ColorSpec::new();
380        new_line.set_fg(Some(termcolor::Color::Red));
381
382        let text = self.text.trim_end();
383        let end = self.span.end.into_usize().min(text.len());
384
385        let before = &text[0..self.span.start.into_usize()].trim_start();
386        let inner = &text[self.span.start.into_usize()..end];
387        let after = &text[end..];
388
389        {
390            let name = self.name;
391            let line = self.line + 1;
392            let start = self.column + 1;
393            let end = start + inner.chars().count();
394            write!(o, "{name}:{line}:{start}-{end}: ")?;
395        }
396
397        write!(o, "{before}")?;
398        o.set_color(&highlight)?;
399        write!(o, "{inner}")?;
400        o.reset()?;
401        write!(o, "{after}")?;
402
403        if self.span.end != end {
404            o.set_color(&new_line)?;
405            write!(o, "\\n")?;
406            o.reset()?;
407        }
408
409        Ok(())
410    }
411}
412
413/// Holder for the name of a source.
414#[derive(Default, Debug, TryClone, PartialEq, Eq)]
415enum SourceName {
416    /// An in-memory source, will use `<memory>` when the source is being
417    /// referred to in diagnostics.
418    #[default]
419    Memory,
420    /// A named source.
421    Name(Box<str>),
422}
423
424#[inline(always)]
425fn line_starts(source: &str) -> impl Iterator<Item = usize> + '_ {
426    iter::once(0).chain(source.match_indices('\n').map(|(i, _)| i + 1))
427}
428
429/// Get the line number and source line for the given source and span.
430fn line_for(source: &Source, span: Span) -> Option<(usize, usize, &str, Span)> {
431    let line_starts = source.line_starts();
432
433    let line = match line_starts.binary_search(&span.start.into_usize()) {
434        Ok(n) => n,
435        Err(n) => n.saturating_sub(1),
436    };
437
438    let start = *line_starts.get(line)?;
439    let end = line.checked_add(1)?;
440
441    let s = if let Some(end) = line_starts.get(end) {
442        source.get(start..*end)?
443    } else {
444        source.get(start..)?
445    };
446
447    let line_end = span.start.into_usize().saturating_sub(start);
448
449    let column = s
450        .get(..line_end)
451        .into_iter()
452        .flat_map(|s| s.chars())
453        .count();
454
455    let start = start.try_into().unwrap();
456
457    Some((
458        line,
459        column,
460        s,
461        Span::new(
462            span.start.saturating_sub(start),
463            span.end.saturating_sub(start),
464        ),
465    ))
466}