sim_codec_algol/parse/
tokenize.rs1use sim_codec::{DecodeBudget, decode_string_literal};
5use sim_kernel::{Error, PrattToken as Token, Result, Trivia};
6
7#[derive(Clone, Debug, PartialEq, Eq)]
10pub struct SpannedToken {
11 pub token: Token,
13 pub start: usize,
15 pub end: usize,
17 pub leading_trivia: Vec<Trivia>,
19}
20
21pub 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
27pub 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}