qualia_core_db/sparql_library/sparql_grammar/
tokenizer.rs1#[derive(Debug, Clone, PartialEq)]
16pub enum Token {
17 Var(String),
20 Iri(String),
22 Prefixed(String, String),
24 Str {
27 value: String,
28 lang: Option<String>,
29 datatype: Option<String>,
30 },
31 Num(String),
34 Bool(bool),
36 Word(String),
39 Punct(char),
41 Op(&'static str),
44 StarOpen,
46 StarClose,
48}
49
50pub fn tokenize(input: &str) -> Result<Vec<Token>, String> {
53 let bytes = input.as_bytes();
54 let n = bytes.len();
55 let mut i = 0usize;
56 let mut out = Vec::new();
57
58 while i < n {
59 let (c, w) = decode_char(bytes, i);
60
61 if c.is_whitespace() {
63 i += w;
64 continue;
65 }
66
67 if c == '#' {
69 while i < n && bytes[i] != b'\n' {
70 i += 1;
71 }
72 continue;
73 }
74
75 if c == '<' && i + 1 < n && bytes[i + 1] == b'<' {
77 out.push(Token::StarOpen);
78 i += 2;
79 continue;
80 }
81 if c == '>' && i + 1 < n && bytes[i + 1] == b'>' {
82 out.push(Token::StarClose);
83 i += 2;
84 continue;
85 }
86
87 if c == '<' {
91 if let Some(close) = find_iri_close(bytes, i + 1) {
92 let iri = std::str::from_utf8(&bytes[i + 1..close])
93 .map_err(|_| "non-utf8 IRI".to_string())?
94 .to_string();
95 out.push(Token::Iri(iri));
96 i = close + 1;
97 continue;
98 }
99 }
101
102 if c == '?' || c == '$' {
104 let start = i;
105 i += 1; while i < n {
107 let (ch, cw) = decode_char(bytes, i);
108 if is_varname_char(ch) {
109 i += cw;
110 } else {
111 break;
112 }
113 }
114 out.push(Token::Var(input[start..i].to_string()));
115 continue;
116 }
117
118 if c == '"' || c == '\'' {
120 let (tok, next) = lex_string(input, bytes, i, c as u8)?;
121 out.push(tok);
122 i = next;
123 continue;
124 }
125
126 if c.is_ascii_digit() || (c == '.' && i + 1 < n && (bytes[i + 1] as char).is_ascii_digit())
129 {
130 let start = i;
131 i += 1;
132 while i < n && {
133 let b = bytes[i] as char;
134 b.is_ascii_digit() || b == '.' || b == 'e' || b == 'E' || b == '+' || b == '-'
135 } {
136 if (bytes[i] == b'+' || bytes[i] == b'-')
138 && !(i > start && (bytes[i - 1] == b'e' || bytes[i - 1] == b'E'))
139 {
140 break;
141 }
142 i += 1;
143 }
144 out.push(Token::Num(input[start..i].to_string()));
145 continue;
146 }
147
148 if let Some(op) = match_two_char_op(bytes, i) {
150 out.push(Token::Op(op));
151 i += 2;
152 continue;
153 }
154
155 match c {
157 '=' | '<' | '>' | '+' | '-' | '*' | '/' | '!' => {
158 out.push(Token::Op(single_char_op(c)));
159 i += 1;
160 continue;
161 }
162 '(' | ')' | '{' | '}' | ',' | ';' | '.' | '[' | ']' => {
163 out.push(Token::Punct(c));
164 i += 1;
165 continue;
166 }
167 _ => {}
168 }
169
170 if is_word_start_char(c) {
173 let start = i;
174 i += w;
175 while i < n {
176 let (ch, cw) = decode_char(bytes, i);
177 if is_word_char(ch) {
178 i += cw;
179 } else {
180 break;
181 }
182 }
183 let word = &input[start..i];
184 if i < n && bytes[i] == b':' {
186 let prefix = word.to_string();
187 i += 1; let local_start = i;
189 while i < n {
190 let (ch, cw) = decode_char(bytes, i);
191 if is_word_char(ch) {
192 i += cw;
193 } else {
194 break;
195 }
196 }
197 let local = input[local_start..i].to_string();
198 out.push(Token::Prefixed(prefix, local));
199 continue;
200 }
201 match word {
202 "true" => out.push(Token::Bool(true)),
203 "false" => out.push(Token::Bool(false)),
204 _ => out.push(Token::Word(word.to_string())),
205 }
206 continue;
207 }
208
209 if c == ':' {
211 let local_start = i + 1;
212 let mut j = local_start;
213 while j < n {
214 let (ch, cw) = decode_char(bytes, j);
215 if is_word_char(ch) {
216 j += cw;
217 } else {
218 break;
219 }
220 }
221 out.push(Token::Prefixed(
222 String::new(),
223 input[local_start..j].to_string(),
224 ));
225 i = j;
226 continue;
227 }
228
229 return Err(format!("unexpected character '{c}' at byte {i}"));
230 }
231
232 Ok(out)
233}
234
235fn find_iri_close(bytes: &[u8], from: usize) -> Option<usize> {
236 let mut i = from;
237 while i < bytes.len() {
238 match bytes[i] {
239 b'>' => return Some(i),
240 b'<' | b' ' | b'\t' | b'\n' | b'\r' | b'"' | b'{' | b'}' | b'|' => return None,
242 _ => i += 1,
243 }
244 }
245 None
246}
247
248fn lex_string(
249 input: &str,
250 bytes: &[u8],
251 start: usize,
252 quote: u8,
253) -> Result<(Token, usize), String> {
254 let n = bytes.len();
255 let mut i = start + 1;
256 let mut value = String::new();
257 while i < n {
258 let b = bytes[i];
259 if b == b'\\' && i + 1 < n {
260 let esc = bytes[i + 1];
261 value.push(match esc {
262 b'n' => '\n',
263 b't' => '\t',
264 b'r' => '\r',
265 b'"' => '"',
266 b'\'' => '\'',
267 b'\\' => '\\',
268 other => other as char,
269 });
270 i += 2;
271 continue;
272 }
273 if b == quote {
274 i += 1; let mut lang = None;
277 let mut datatype = None;
278 if i < n && bytes[i] == b'@' {
279 let ls = i + 1;
280 let mut j = ls;
281 while j < n {
282 let (ch, cw) = decode_char(bytes, j);
283 if is_word_char(ch) || ch == '-' {
284 j += cw;
285 } else {
286 break;
287 }
288 }
289 lang = Some(input[ls..j].to_string());
290 i = j;
291 } else if i + 1 < n && bytes[i] == b'^' && bytes[i + 1] == b'^' {
292 i += 2;
293 if i < n && bytes[i] == b'<' {
294 if let Some(close) = find_iri_close(bytes, i + 1) {
295 datatype = Some(input[i + 1..close].to_string());
296 i = close + 1;
297 }
298 } else {
299 let ds = i;
300 while i < n {
301 let (ch, cw) = decode_char(bytes, i);
302 if is_word_char(ch) || ch == ':' {
303 i += cw;
304 } else {
305 break;
306 }
307 }
308 datatype = Some(input[ds..i].to_string());
309 }
310 }
311 return Ok((
312 Token::Str {
313 value,
314 lang,
315 datatype,
316 },
317 i,
318 ));
319 }
320 let (ch, cw) = decode_char(bytes, i);
322 value.push(ch);
323 i += cw;
324 }
325 Err("unterminated string literal".to_string())
326}
327
328fn match_two_char_op(bytes: &[u8], i: usize) -> Option<&'static str> {
329 if i + 1 >= bytes.len() {
330 return None;
331 }
332 match (bytes[i], bytes[i + 1]) {
333 (b'|', b'|') => Some("||"),
334 (b'&', b'&') => Some("&&"),
335 (b'!', b'=') => Some("!="),
336 (b'<', b'=') => Some("<="),
337 (b'>', b'=') => Some(">="),
338 _ => None,
339 }
340}
341
342fn single_char_op(c: char) -> &'static str {
343 match c {
344 '=' => "=",
345 '<' => "<",
346 '>' => ">",
347 '+' => "+",
348 '-' => "-",
349 '*' => "*",
350 '/' => "/",
351 '!' => "!",
352 _ => unreachable!(),
353 }
354}
355
356#[inline]
359fn decode_char(bytes: &[u8], i: usize) -> (char, usize) {
360 let b = bytes[i];
361 if b < 0x80 {
362 return (b as char, 1);
363 }
364 let remaining = &bytes[i..];
366 match std::str::from_utf8(remaining) {
367 Ok(s) => {
368 let c = s.chars().next().unwrap_or(b as char);
369 (c, c.len_utf8())
370 }
371 Err(e) => {
372 let valid_len = e.valid_up_to();
374 if valid_len > 0 {
375 let s = std::str::from_utf8(&remaining[..valid_len]).unwrap();
376 let c = s.chars().next().unwrap();
377 (c, c.len_utf8())
378 } else {
379 (b as char, 1)
381 }
382 }
383 }
384}
385
386fn is_varname_char(c: char) -> bool {
388 c.is_alphanumeric() || c == '_'
389}
390
391fn is_word_start_char(c: char) -> bool {
393 c.is_alphabetic() || c == '_'
394}
395
396fn is_word_char(c: char) -> bool {
398 c.is_alphanumeric() || c == '_' || c == '-' || c == '.'
399}
400
401#[cfg(test)]
402mod tests {
403 use super::*;
404
405 #[test]
406 fn tokenize_filter_expression() {
407 let toks = tokenize("?age >= 18 && ?name = \"Alice\"").unwrap();
408 assert_eq!(
409 toks,
410 vec![
411 Token::Var("?age".into()),
412 Token::Op(">="),
413 Token::Num("18".into()),
414 Token::Op("&&"),
415 Token::Var("?name".into()),
416 Token::Op("="),
417 Token::Str {
418 value: "Alice".into(),
419 lang: None,
420 datatype: None
421 },
422 ]
423 );
424 }
425
426 #[test]
427 fn tokenize_iri_and_prefixed() {
428 let toks = tokenize("?s <http://example.org/p> foaf:name").unwrap();
429 assert_eq!(toks[0], Token::Var("?s".into()));
430 assert_eq!(toks[1], Token::Iri("http://example.org/p".into()));
431 assert_eq!(toks[2], Token::Prefixed("foaf".into(), "name".into()));
432 }
433
434 #[test]
435 fn tokenize_star_triple() {
436 let toks = tokenize("<< ?s ?p ?o >>").unwrap();
437 assert_eq!(toks[0], Token::StarOpen);
438 assert_eq!(toks[4], Token::StarClose);
439 }
440
441 #[test]
442 fn tokenize_function_call_and_punct() {
443 let toks = tokenize("REGEX(?x, \"^a\")").unwrap();
444 assert_eq!(toks[0], Token::Word("REGEX".into()));
445 assert_eq!(toks[1], Token::Punct('('));
446 assert_eq!(toks[2], Token::Var("?x".into()));
447 assert_eq!(toks[3], Token::Punct(','));
448 assert_eq!(
449 toks[4],
450 Token::Str {
451 value: "^a".into(),
452 lang: None,
453 datatype: None
454 }
455 );
456 assert_eq!(toks[5], Token::Punct(')'));
457 }
458
459 #[test]
460 fn tokenize_lt_operator_vs_iri() {
461 let toks = tokenize("?a < ?b").unwrap();
463 assert_eq!(
464 toks,
465 vec![
466 Token::Var("?a".into()),
467 Token::Op("<"),
468 Token::Var("?b".into())
469 ]
470 );
471 }
472
473 #[test]
474 fn tokenize_typed_literal() {
475 let toks = tokenize("\"5\"^^<http://www.w3.org/2001/XMLSchema#integer>").unwrap();
476 assert_eq!(
477 toks[0],
478 Token::Str {
479 value: "5".into(),
480 lang: None,
481 datatype: Some("http://www.w3.org/2001/XMLSchema#integer".into())
482 }
483 );
484 }
485
486 #[test]
487 fn tokenize_lang_literal() {
488 let toks = tokenize("\"chat\"@fr").unwrap();
489 assert_eq!(
490 toks[0],
491 Token::Str {
492 value: "chat".into(),
493 lang: Some("fr".into()),
494 datatype: None
495 }
496 );
497 }
498
499 #[test]
500 fn tokenize_unicode_variable_name() {
501 let toks = tokenize("?名前").unwrap();
502 assert_eq!(toks[0], Token::Var("?名前".into()));
503 }
504
505 #[test]
506 fn tokenize_unicode_prefixed_name() {
507 let toks = tokenize("日本:東京").unwrap();
508 assert_eq!(toks[0], Token::Prefixed("日本".into(), "東京".into()));
509 }
510
511 #[test]
512 fn tokenize_unicode_string_literal() {
513 let toks = tokenize("\"你好世界\"").unwrap();
514 assert_eq!(
515 toks[0],
516 Token::Str {
517 value: "你好世界".into(),
518 lang: None,
519 datatype: None
520 }
521 );
522 }
523
524 #[test]
525 fn tokenize_unicode_whitespace() {
526 let toks = tokenize("?x\u{3000}?y").unwrap();
528 assert_eq!(toks[0], Token::Var("?x".into()));
529 assert_eq!(toks[1], Token::Var("?y".into()));
530 }
531}