1use crate::error::{ParseError, Span};
13
14#[derive(Debug, Clone, PartialEq)]
16pub enum TokenKind {
17 LParen,
19 RParen,
21 Symbol(String),
24 Keyword(String),
27 Variable(String),
30 Number(f64),
32}
33
34#[derive(Debug, Clone, PartialEq)]
36pub struct Token {
37 pub kind: TokenKind,
39 pub span: Span,
41}
42
43impl Token {
44 pub fn symbol_eq(&self, s: &str) -> bool {
50 matches!(&self.kind, TokenKind::Symbol(sym) if sym.eq_ignore_ascii_case(s))
51 }
52
53 pub fn keyword_eq(&self, s: &str) -> bool {
59 matches!(&self.kind, TokenKind::Keyword(kw) if kw.eq_ignore_ascii_case(s))
60 }
61
62 pub fn variable_eq(&self, s: &str) -> bool {
68 matches!(&self.kind, TokenKind::Variable(v) if v.eq_ignore_ascii_case(s))
69 }
70}
71
72pub fn tokenize(input: &str) -> Result<Vec<Token>, ParseError> {
90 let mut tokens = Vec::new();
91 let bytes = input.as_bytes();
92 let mut pos = 0;
93 let mut line = 1usize;
94 let mut col = 1usize;
95
96 while pos < bytes.len() {
97 let b = bytes[pos];
98
99 if b.is_ascii_whitespace() {
101 if b == b'\n' {
102 line += 1;
103 col = 1;
104 } else {
105 col += 1;
106 }
107 pos += 1;
108 continue;
109 }
110
111 if b == b';' {
113 while pos < bytes.len() && bytes[pos] != b'\n' {
114 pos += 1;
115 }
116 continue;
117 }
118
119 let span = Span::new(pos, line, col);
120
121 if b == b'(' {
122 tokens.push(Token {
123 kind: TokenKind::LParen,
124 span,
125 });
126 pos += 1;
127 col += 1;
128 continue;
129 }
130
131 if b == b')' {
132 tokens.push(Token {
133 kind: TokenKind::RParen,
134 span,
135 });
136 pos += 1;
137 col += 1;
138 continue;
139 }
140
141 if b == b':' {
143 pos += 1;
144 col += 1;
145 let start = pos;
146 while pos < bytes.len() && is_symbol_char(bytes[pos]) {
147 pos += 1;
148 col += 1;
149 }
150 let kw = std::str::from_utf8(&bytes[start..pos])
151 .unwrap()
152 .to_ascii_lowercase();
153 tokens.push(Token {
154 kind: TokenKind::Keyword(kw),
155 span,
156 });
157 continue;
158 }
159
160 if b == b'?' {
162 let start = pos;
163 pos += 1;
164 col += 1;
165 while pos < bytes.len() && is_symbol_char(bytes[pos]) {
166 pos += 1;
167 col += 1;
168 }
169 let var = std::str::from_utf8(&bytes[start..pos])
170 .unwrap()
171 .to_lowercase();
172 tokens.push(Token {
173 kind: TokenKind::Variable(var),
174 span,
175 });
176 continue;
177 }
178
179 if b.is_ascii_digit()
183 || (b == b'.' && pos + 1 < bytes.len() && bytes[pos + 1].is_ascii_digit())
184 {
185 let start = pos;
186 while pos < bytes.len() && (bytes[pos].is_ascii_digit() || bytes[pos] == b'.') {
187 pos += 1;
188 col += 1;
189 }
190 let num_str = std::str::from_utf8(&bytes[start..pos]).unwrap();
191 let num: f64 = num_str
192 .parse()
193 .map_err(|_| ParseError::new(format!("invalid number: {num_str}"), span))?;
194 tokens.push(Token {
195 kind: TokenKind::Number(num),
196 span,
197 });
198 continue;
199 }
200
201 if b == b'"' {
203 pos += 1;
204 col += 1;
205 let start = pos;
206 while pos < bytes.len() && bytes[pos] != b'"' {
207 if bytes[pos] == b'\n' {
208 line += 1;
209 col = 1;
210 } else {
211 col += 1;
212 }
213 pos += 1;
214 }
215 let s = std::str::from_utf8(&bytes[start..pos]).unwrap().to_string();
216 if pos < bytes.len() {
217 pos += 1; col += 1;
219 }
220 tokens.push(Token {
221 kind: TokenKind::Symbol(s),
222 span,
223 });
224 continue;
225 }
226
227 if is_symbol_start(b) {
229 let start = pos;
230 while pos < bytes.len() && is_symbol_char(bytes[pos]) {
231 pos += 1;
232 col += 1;
233 }
234 let sym = std::str::from_utf8(&bytes[start..pos])
235 .unwrap()
236 .to_lowercase();
237 tokens.push(Token {
238 kind: TokenKind::Symbol(sym),
239 span,
240 });
241 continue;
242 }
243
244 return Err(ParseError::new(
245 format!("unexpected character: '{}'", b as char),
246 span,
247 ));
248 }
249
250 Ok(tokens)
251}
252
253fn is_symbol_start(b: u8) -> bool {
256 b.is_ascii_alphabetic()
257 || b == b'_'
258 || b == b'-'
259 || b == b'+'
260 || b == b'*'
261 || b == b'/'
262 || b == b'='
263 || b == b'<'
264 || b == b'>'
265 || b == b'#'
266}
267
268fn is_symbol_char(b: u8) -> bool {
271 b.is_ascii_alphanumeric()
272 || b == b'_'
273 || b == b'-'
274 || b == b'+'
275 || b == b'*'
276 || b == b'/'
277 || b == b'='
278 || b == b'<'
279 || b == b'>'
280 || b == b'.'
281 || b == b'#'
282}
283
284#[cfg(test)]
285mod tests {
286 use super::*;
287
288 #[test]
289 fn test_basic_tokens() {
290 let tokens = tokenize("(define (domain test))").unwrap();
291 assert_eq!(tokens.len(), 7);
292 assert_eq!(tokens[0].kind, TokenKind::LParen);
293 assert!(tokens[1].symbol_eq("define"));
294 assert_eq!(tokens[2].kind, TokenKind::LParen);
295 assert!(tokens[3].symbol_eq("domain"));
296 assert!(tokens[4].symbol_eq("test"));
297 assert_eq!(tokens[5].kind, TokenKind::RParen);
298 assert_eq!(tokens[6].kind, TokenKind::RParen);
299 }
300
301 #[test]
302 fn test_keywords_and_variables() {
303 let tokens = tokenize("(:requirements :typing) (?x - type1)").unwrap();
304 assert!(tokens[1].keyword_eq("requirements"));
305 assert!(!tokens[1].keyword_eq("strips"));
306 assert!(tokens[2].keyword_eq("typing"));
307 assert!(tokens[5].variable_eq("?x"));
308 assert!(!tokens[5].variable_eq("?y"));
309 }
310
311 #[test]
312 fn test_numbers() {
313 let tokens = tokenize("(= (f) 3.57)").unwrap();
315 assert_eq!(tokens.len(), 7);
316 assert_eq!(tokens[5].kind, TokenKind::Number(3.57));
317 }
318
319 #[test]
320 fn test_comments_skipped() {
321 let tokens = tokenize("; comment\n(define)").unwrap();
322 assert_eq!(tokens.len(), 3);
323 assert_eq!(tokens[0].kind, TokenKind::LParen);
324 }
325
326 #[test]
327 fn test_negative_symbol() {
328 let tokens = tokenize("(- 3 2)").unwrap();
329 assert!(tokens[1].symbol_eq("-"));
330 assert_eq!(tokens[2].kind, TokenKind::Number(3.0));
331 }
332
333 #[test]
334 fn test_strings_with_newlines_symbols_hash_and_unexpected_char() {
335 let tokens = tokenize("\"line one\nline two\" #tag").unwrap();
336 assert_eq!(
337 tokens[0].kind,
338 TokenKind::Symbol("line one\nline two".to_string())
339 );
340 assert!(tokens[1].symbol_eq("#tag"));
341
342 let err = tokenize("@").unwrap_err();
343 assert!(err.message.contains("unexpected character"));
344
345 let err = tokenize("1.2.3").unwrap_err();
346 assert!(err.message.contains("invalid number"));
347 }
348}