1use crate::error::{ParseError, Span};
14
15#[derive(Debug, Clone, PartialEq)]
17pub enum TokenKind {
18 LParen,
20 RParen,
22 LBrace,
24 RBrace,
26 LBracket,
28 RBracket,
30 Comma,
32 Colon,
34 DotDot,
36 Plus,
38 Minus,
40 Star,
42 Slash,
44 At,
46 Le,
48 Ge,
50 Lt,
52 Gt,
54 EqEq,
56 Ne,
58 Eq,
60 Number(i64),
62 Float(f64),
64 Ident(String),
66}
67
68#[derive(Debug, Clone, PartialEq)]
70pub struct Token {
71 pub kind: TokenKind,
73 pub span: Span,
75}
76
77pub fn tokenize(input: &str) -> Result<Vec<Token>, ParseError> {
83 let mut tokens = Vec::new();
84 let bytes = input.as_bytes();
85 let mut pos = 0;
86 let mut line = 1usize;
87 let mut col = 1usize;
88
89 while pos < bytes.len() {
90 let b = bytes[pos];
91
92 if b.is_ascii_whitespace() {
94 if b == b'\n' {
95 line += 1;
96 col = 1;
97 } else {
98 col += 1;
99 }
100 pos += 1;
101 continue;
102 }
103
104 if b == b'/' && pos + 1 < bytes.len() && bytes[pos + 1] == b'/' {
106 while pos < bytes.len() && bytes[pos] != b'\n' {
108 pos += 1;
109 }
110 continue;
111 }
112
113 let span = Span::new(pos, line, col);
114
115 if pos + 1 < bytes.len() {
117 let next = bytes[pos + 1];
118 let two_char = match (b, next) {
119 (b'<', b'=') => Some(TokenKind::Le),
120 (b'>', b'=') => Some(TokenKind::Ge),
121 (b'=', b'=') => Some(TokenKind::EqEq),
122 (b'!', b'=') => Some(TokenKind::Ne),
123 (b'.', b'.') => Some(TokenKind::DotDot),
124 _ => None,
125 };
126 if let Some(kind) = two_char {
127 tokens.push(Token { kind, span });
128 pos += 2;
129 col += 2;
130 continue;
131 }
132 }
133
134 let single = match b {
136 b'(' => Some(TokenKind::LParen),
137 b')' => Some(TokenKind::RParen),
138 b'{' => Some(TokenKind::LBrace),
139 b'}' => Some(TokenKind::RBrace),
140 b'[' => Some(TokenKind::LBracket),
141 b']' => Some(TokenKind::RBracket),
142 b',' => Some(TokenKind::Comma),
143 b':' => Some(TokenKind::Colon),
144 b'+' => Some(TokenKind::Plus),
145 b'-' => Some(TokenKind::Minus),
146 b'*' => Some(TokenKind::Star),
147 b'/' => Some(TokenKind::Slash),
148 b'@' => Some(TokenKind::At),
149 b'<' => Some(TokenKind::Lt),
150 b'>' => Some(TokenKind::Gt),
151 b'=' => Some(TokenKind::Eq),
152 _ => None,
153 };
154 if let Some(kind) = single {
155 tokens.push(Token { kind, span });
156 pos += 1;
157 col += 1;
158 continue;
159 }
160
161 if b.is_ascii_digit() {
163 let start = pos;
164 while pos < bytes.len() && bytes[pos].is_ascii_digit() {
165 pos += 1;
166 }
167 let is_float = pos < bytes.len()
169 && bytes[pos] == b'.'
170 && pos + 1 < bytes.len()
171 && bytes[pos + 1].is_ascii_digit();
172 if is_float {
173 pos += 1; while pos < bytes.len() && bytes[pos].is_ascii_digit() {
175 pos += 1;
176 }
177 let text = &input[start..pos];
178 let value: f64 = match text.parse() {
179 Ok(value) => value,
180 Err(_) => {
181 return Err(ParseError::new(
182 format!("invalid float literal: {text}"),
183 span,
184 ));
185 }
186 };
187 tokens.push(Token {
188 kind: TokenKind::Float(value),
189 span,
190 });
191 } else {
192 let text = &input[start..pos];
193 let value: i64 = text.parse().map_err(|_| {
194 ParseError::new(format!("invalid number literal: {text}"), span)
195 })?;
196 tokens.push(Token {
197 kind: TokenKind::Number(value),
198 span,
199 });
200 }
201 col += pos - start;
202 continue;
203 }
204
205 if b.is_ascii_alphabetic() || b == b'_' {
207 let start = pos;
208 while pos < bytes.len() && (bytes[pos].is_ascii_alphanumeric() || bytes[pos] == b'_') {
209 pos += 1;
210 }
211 let text = input[start..pos].to_string();
212 tokens.push(Token {
213 kind: TokenKind::Ident(text),
214 span,
215 });
216 col += pos - start;
217 continue;
218 }
219
220 return Err(ParseError::new(
221 format!("unexpected character: '{}'", b as char),
222 span,
223 ));
224 }
225
226 Ok(tokens)
227}
228
229#[cfg(test)]
230mod tests {
231 use super::*;
232
233 #[test]
234 fn test_basic_tokens() {
235 let tokens = tokenize("model foo").unwrap();
236 assert_eq!(tokens.len(), 2);
237 assert_eq!(tokens[0].kind, TokenKind::Ident("model".to_string()));
238 assert_eq!(tokens[1].kind, TokenKind::Ident("foo".to_string()));
239 }
240
241 #[test]
242 fn test_operators() {
243 let tokens = tokenize("<= >= == != < > = ..").unwrap();
244 let kinds: Vec<_> = tokens.iter().map(|t| &t.kind).collect();
245 assert_eq!(
246 kinds,
247 vec![
248 &TokenKind::Le,
249 &TokenKind::Ge,
250 &TokenKind::EqEq,
251 &TokenKind::Ne,
252 &TokenKind::Lt,
253 &TokenKind::Gt,
254 &TokenKind::Eq,
255 &TokenKind::DotDot,
256 ]
257 );
258 }
259
260 #[test]
261 fn test_punctuation() {
262 let tokens = tokenize("( ) { } [ ] , :").unwrap();
263 assert_eq!(tokens.len(), 8);
264 assert_eq!(tokens[0].kind, TokenKind::LParen);
265 assert_eq!(tokens[1].kind, TokenKind::RParen);
266 assert_eq!(tokens[2].kind, TokenKind::LBrace);
267 assert_eq!(tokens[3].kind, TokenKind::RBrace);
268 assert_eq!(tokens[4].kind, TokenKind::LBracket);
269 assert_eq!(tokens[5].kind, TokenKind::RBracket);
270 assert_eq!(tokens[6].kind, TokenKind::Comma);
271 assert_eq!(tokens[7].kind, TokenKind::Colon);
272 }
273
274 #[test]
275 fn test_numbers() {
276 let tokens = tokenize("42 0 100").unwrap();
277 assert_eq!(tokens[0].kind, TokenKind::Number(42));
278 assert_eq!(tokens[1].kind, TokenKind::Number(0));
279 assert_eq!(tokens[2].kind, TokenKind::Number(100));
280 }
281
282 #[test]
283 fn test_line_comment() {
284 let tokens = tokenize("model foo // this is a comment\nvariables").unwrap();
285 assert_eq!(tokens.len(), 3);
286 assert_eq!(tokens[0].kind, TokenKind::Ident("model".to_string()));
287 assert_eq!(tokens[1].kind, TokenKind::Ident("foo".to_string()));
288 assert_eq!(tokens[2].kind, TokenKind::Ident("variables".to_string()));
289 }
290
291 #[test]
292 fn test_span_tracking() {
293 let tokens = tokenize("a\nb").unwrap();
294 assert_eq!(tokens[0].span, Span::new(0, 1, 1));
295 assert_eq!(tokens[1].span, Span::new(2, 2, 1));
296 }
297
298 #[test]
299 fn test_arithmetic_operators() {
300 let tokens = tokenize("+ - *").unwrap();
301 assert_eq!(tokens[0].kind, TokenKind::Plus);
302 assert_eq!(tokens[1].kind, TokenKind::Minus);
303 assert_eq!(tokens[2].kind, TokenKind::Star);
304 }
305
306 #[test]
307 fn test_unexpected_char() {
308 let err = tokenize("model #foo").unwrap_err();
309 assert!(err.message.contains("unexpected character"));
310 }
311
312 #[test]
313 fn test_float_literals() {
314 let tokens = tokenize("3.25 0.5 100.0").unwrap();
315 assert_eq!(tokens[0].kind, TokenKind::Float(3.25));
316 assert_eq!(tokens[1].kind, TokenKind::Float(0.5));
317 assert_eq!(tokens[2].kind, TokenKind::Float(100.0));
318 }
319
320 #[test]
321 fn test_integer_followed_by_dotdot_not_float() {
322 let tokens = tokenize("0..10").unwrap();
324 assert_eq!(tokens[0].kind, TokenKind::Number(0));
325 assert_eq!(tokens[1].kind, TokenKind::DotDot);
326 assert_eq!(tokens[2].kind, TokenKind::Number(10));
327 }
328
329 #[test]
330 fn test_slash_operator() {
331 let tokens = tokenize("x / 2").unwrap();
332 assert_eq!(tokens[1].kind, TokenKind::Slash);
333 }
334
335 #[test]
336 fn test_slash_vs_comment() {
337 let tokens = tokenize("x / y // comment").unwrap();
339 assert_eq!(tokens.len(), 3);
340 assert_eq!(tokens[1].kind, TokenKind::Slash);
341 }
342
343 #[test]
344 fn test_at_token() {
345 let tokens = tokenize("@model lp").unwrap();
346 assert_eq!(tokens[0].kind, TokenKind::At);
347 assert_eq!(tokens[1].kind, TokenKind::Ident("model".to_string()));
348 assert_eq!(tokens[2].kind, TokenKind::Ident("lp".to_string()));
349 }
350
351 #[test]
352 fn test_integer_overflow_reports_error() {
353 let err = tokenize("999999999999999999999999999999999999").unwrap_err();
354 assert!(err.message.contains("invalid number literal"));
355 }
356}