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