Skip to main content

abyss_core/parser/
helpers.rs

1use std::sync::Arc;
2
3use chumsky::{error::Rich, extra, prelude::*, span::SimpleSpan as ChumskySpan, text};
4
5use crate::ast::LineInfo;
6
7use super::SimpleSpan;
8
9/// Represents a mapping between byte offsets and (line, column) positions.
10#[derive(Debug, Clone)]
11pub struct LineMap {
12    line_starts: Arc<Vec<usize>>,
13}
14
15impl LineMap {
16    pub fn new(source: &str) -> Self {
17        let mut starts = Vec::with_capacity(source.lines().count() + 1);
18        starts.push(0);
19        for (idx, ch) in source.char_indices() {
20            if ch == '\n' {
21                starts.push(idx + ch.len_utf8());
22            }
23        }
24        LineMap {
25            line_starts: Arc::new(starts),
26        }
27    }
28
29    pub fn line_col(&self, offset: usize) -> (usize, usize) {
30        let line_idx = match self.line_starts.binary_search(&offset) {
31            Ok(idx) => idx,
32            Err(idx) => idx.saturating_sub(1),
33        };
34        let line_start = self.line_starts[line_idx];
35        (line_idx + 1, offset - line_start + 1)
36    }
37
38    pub fn line_info(&self, span: SimpleSpan<usize>) -> Option<LineInfo> {
39        let (line, column) = self.line_col(span.start());
40        Some(LineInfo::new(line, column))
41    }
42}
43
44type LexerExtra<'src> = extra::Err<Rich<'src, char, ChumskySpan<usize>>>;
45
46/// Produces a parser that skips AbySS whitespace.
47pub fn abyss_whitespace<'src>() -> impl Parser<'src, &'src str, (), LexerExtra<'src>> + Clone {
48    text::whitespace::<_, LexerExtra<'src>>().to(())
49}
50
51/// Helper to attach line info to AST nodes.
52pub fn attach_line_info(map: &LineMap, span: SimpleSpan<usize>) -> Option<LineInfo> {
53    map.line_info(span)
54}
55
56/// Replace comments with whitespace of equal length so token spans align with the original source.
57pub fn scrub_comments_preserve_layout(source: &str) -> String {
58    let mut result = String::with_capacity(source.len());
59    let mut chars = source.chars().peekable();
60
61    while let Some(ch) = chars.next() {
62        if ch == '/'
63            && let Some(&next) = chars.peek()
64        {
65            if next == '/' {
66                // Single-line comment: consume until newline, keep newline intact.
67                result.push(' '); // replace first '/'
68                chars.next(); // consume second '/'
69                result.push(' ');
70
71                while let Some(&c) = chars.peek() {
72                    chars.next();
73                    if c == '\n' {
74                        result.push('\n');
75                        break;
76                    }
77                    result.push(' ');
78                }
79
80                continue;
81            } else if next == '*' {
82                // Block comment: consume until closing */ while preserving newlines.
83                result.push(' '); // first '/'
84                chars.next(); // consume '*'
85                result.push(' ');
86
87                let mut prev = '\0';
88                for c in chars.by_ref() {
89                    if c == '\n' {
90                        result.push('\n');
91                    } else {
92                        result.push(' ');
93                    }
94
95                    if prev == '*' && c == '/' {
96                        break;
97                    }
98
99                    prev = c;
100                }
101
102                continue;
103            }
104        }
105
106        result.push(ch);
107    }
108
109    result
110}