Skip to main content

sim_codec_algol/parse/
tokenize.rs

1//! Tokenizer for Algol source: scans infix text into spanned tokens carrying
2//! span offsets and leading trivia for round-tripping.
3
4use sim_codec::{DecodeBudget, decode_string_literal};
5use sim_kernel::{Error, PrattToken as Token, Result, Trivia};
6
7/// A Pratt token paired with its byte span and the trivia (whitespace and
8/// comments) that preceded it, so source layout can be reconstructed on encode.
9#[derive(Clone, Debug, PartialEq, Eq)]
10pub struct SpannedToken {
11    /// The scanned token.
12    pub token: Token,
13    /// Byte offset where the token starts in the source.
14    pub start: usize,
15    /// Byte offset just past the end of the token.
16    pub end: usize,
17    /// Whitespace and comment trivia immediately preceding the token.
18    pub leading_trivia: Vec<Trivia>,
19}
20
21/// Tokenizes Algol `source` into spanned tokens under a default decode budget.
22pub fn tokenize_algol_spanned(source: &str) -> Result<Vec<SpannedToken>> {
23    let mut budget = DecodeBudget::new(sim_codec::DecodeLimits::default());
24    tokenize_algol_spanned_with_budget(source, &mut budget)
25}
26
27/// Tokenizes Algol `source` into spanned tokens under an explicit `budget`.
28///
29/// Scans identifiers, numbers, string literals, operator runs, parentheses, and
30/// commas, attaching leading whitespace and comment trivia to each token. The
31/// budget bounds trivia and string sizes; an unterminated block comment or an
32/// unexpected character is reported as an error.
33pub fn tokenize_algol_spanned_with_budget(
34    source: &str,
35    budget: &mut DecodeBudget,
36) -> Result<Vec<SpannedToken>> {
37    let chars = source.char_indices().collect::<Vec<_>>();
38    let mut index = 0;
39    let mut tokens = Vec::new();
40    let source_len = source.len();
41
42    let char_at = |index: usize| chars.get(index).map(|(_, ch)| *ch);
43    let byte_at = |index: usize| chars.get(index).map(|(offset, _)| *offset);
44    let end_of = |index: usize| byte_at(index).unwrap_or(source_len);
45
46    while index < chars.len() {
47        let mut leading_trivia = Vec::new();
48        let ch = char_at(index).unwrap();
49        if ch.is_whitespace()
50            || (ch == '/' && char_at(index + 1).is_some_and(|next| next == '/' || next == '*'))
51        {
52            while let Some(ch) = char_at(index) {
53                if ch.is_whitespace() {
54                    let start = byte_at(index).unwrap();
55                    index += 1;
56                    while index < chars.len() && char_at(index).is_some_and(char::is_whitespace) {
57                        index += 1;
58                    }
59                    budget.add_trivia(sim_kernel::CodecId(0))?;
60                    leading_trivia
61                        .push(Trivia::Whitespace(source[start..end_of(index)].to_owned()));
62                    continue;
63                }
64                if ch == '/' && char_at(index + 1) == Some('/') {
65                    let start = byte_at(index).unwrap();
66                    index += 2;
67                    while index < chars.len() && char_at(index) != Some('\n') {
68                        index += 1;
69                    }
70                    budget.add_trivia(sim_kernel::CodecId(0))?;
71                    leading_trivia
72                        .push(Trivia::LineComment(source[start..end_of(index)].to_owned()));
73                    continue;
74                }
75                if ch == '/' && char_at(index + 1) == Some('*') {
76                    let start = byte_at(index).unwrap();
77                    index += 2;
78                    let mut closed = false;
79                    while index < chars.len() {
80                        if char_at(index) == Some('*') && char_at(index + 1) == Some('/') {
81                            index += 2;
82                            closed = true;
83                            break;
84                        }
85                        index += 1;
86                    }
87                    if !closed {
88                        return Err(Error::CodecError {
89                            codec: sim_kernel::CodecId(0),
90                            message: "unterminated algol block comment".to_owned(),
91                        });
92                    }
93                    budget.add_trivia(sim_kernel::CodecId(0))?;
94                    leading_trivia.push(Trivia::BlockComment(
95                        source[start..end_of(index.min(chars.len()))].to_owned(),
96                    ));
97                    continue;
98                }
99                break;
100            }
101            if index >= chars.len() {
102                break;
103            }
104        }
105        let ch = char_at(index).unwrap();
106        if ch == '(' {
107            let start = byte_at(index).unwrap();
108            index += 1;
109            tokens.push(SpannedToken {
110                token: Token::OpenParen,
111                start,
112                end: end_of(index),
113                leading_trivia,
114            });
115            continue;
116        }
117        if ch == ')' {
118            let start = byte_at(index).unwrap();
119            index += 1;
120            tokens.push(SpannedToken {
121                token: Token::CloseParen,
122                start,
123                end: end_of(index),
124                leading_trivia,
125            });
126            continue;
127        }
128        if ch == ',' {
129            let start = byte_at(index).unwrap();
130            index += 1;
131            tokens.push(SpannedToken {
132                token: Token::Comma,
133                start,
134                end: end_of(index),
135                leading_trivia,
136            });
137            continue;
138        }
139        if ch == '"' {
140            let start = byte_at(index).unwrap();
141            index += 1;
142            let mut escaped = false;
143            while index < chars.len() {
144                let next = char_at(index).unwrap();
145                if !escaped && next == '"' {
146                    index += 1;
147                    break;
148                }
149                escaped = !escaped && next == '\\';
150                index += 1;
151            }
152            let raw = &source[start..end_of(index)];
153            let value = decode_string_literal(sim_kernel::CodecId(0), raw)?;
154            budget.check_string_bytes(sim_kernel::CodecId(0), value.len())?;
155            tokens.push(SpannedToken {
156                token: Token::String(value),
157                start,
158                end: end_of(index),
159                leading_trivia,
160            });
161            continue;
162        }
163        if ch.is_ascii_digit()
164            || (ch == '.' && char_at(index + 1).is_some_and(|c| c.is_ascii_digit()))
165        {
166            let start = index;
167            index += 1;
168            while index < chars.len()
169                && char_at(index).is_some_and(|c| c.is_ascii_digit() || c == '.')
170            {
171                index += 1;
172            }
173            budget.check_string_bytes(
174                sim_kernel::CodecId(0),
175                source[byte_at(start).unwrap()..end_of(index)].len(),
176            )?;
177            tokens.push(SpannedToken {
178                token: Token::Number(source[byte_at(start).unwrap()..end_of(index)].to_owned()),
179                start: byte_at(start).unwrap(),
180                end: end_of(index),
181                leading_trivia,
182            });
183            continue;
184        }
185        if is_ident_start(ch) {
186            let start = index;
187            index += 1;
188            while index < chars.len() && char_at(index).is_some_and(is_ident_continue) {
189                index += 1;
190            }
191            tokens.push(SpannedToken {
192                token: Token::Ident(source[byte_at(start).unwrap()..end_of(index)].to_owned()),
193                start: byte_at(start).unwrap(),
194                end: end_of(index),
195                leading_trivia,
196            });
197            continue;
198        }
199        if is_operator_char(ch) {
200            let start = index;
201            index += 1;
202            while index < chars.len() && char_at(index).is_some_and(is_operator_char) {
203                index += 1;
204            }
205            tokens.push(SpannedToken {
206                token: Token::Operator(source[byte_at(start).unwrap()..end_of(index)].to_owned()),
207                start: byte_at(start).unwrap(),
208                end: end_of(index),
209                leading_trivia,
210            });
211            continue;
212        }
213        return Err(Error::Eval(format!("unexpected algol character {}", ch)));
214    }
215
216    Ok(tokens)
217}
218
219fn is_ident_start(ch: char) -> bool {
220    ch.is_ascii_alphabetic() || ch == '_'
221}
222
223fn is_ident_continue(ch: char) -> bool {
224    ch.is_ascii_alphanumeric() || matches!(ch, '_' | '.' | ':' | '/' | '?')
225}
226
227fn is_operator_char(ch: char) -> bool {
228    matches!(
229        ch,
230        '+' | '-' | '*' | '/' | '^' | '!' | '%' | '=' | '<' | '>' | '&' | '|' | '~'
231    )
232}