Skip to main content

reifydb_testing/testscript/
parser.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2025 ReifyDB
3
4// This file includes and modifies code from the toydb project (https://github.com/erikgrinaker/toydb),
5// originally licensed under the Apache License, Version 2.0.
6// Original copyright:
7//   Copyright (c) 2024 Erik Grinaker
8//
9// The original Apache License can be found at:
10//   http://www.apache.org/licenses/LICENSE-2.0
11
12use std::{collections::HashSet, error, fmt};
13
14use crate::testscript::command::{Argument, Block, Command};
15
16#[derive(Debug, Clone)]
17pub struct ParseError {
18	pub message: String,
19	pub line: u32,
20	pub column: usize,
21	pub input: LocatedSpan,
22	pub code: String,
23}
24
25impl fmt::Display for ParseError {
26	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27		write!(f, "Parse error at line {}:{}: {}", self.line, self.column, self.message)
28	}
29}
30
31impl error::Error for ParseError {}
32
33#[derive(Debug, Clone)]
34pub struct LocatedSpan {
35	column: usize,
36	line: u32,
37	line_text: String,
38}
39
40impl LocatedSpan {
41	fn new(_line_start: usize, column: usize, line: u32, line_text: String) -> Self {
42		LocatedSpan {
43			column,
44			line,
45			line_text,
46		}
47	}
48
49	pub fn location_line(&self) -> u32 {
50		self.line
51	}
52
53	pub fn get_column(&self) -> usize {
54		self.column
55	}
56
57	pub fn get_utf8_column(&self) -> usize {
58		self.column
59	}
60
61	pub fn get_line_beginning(&self) -> &[u8] {
62		self.line_text.as_bytes()
63	}
64}
65
66pub(crate) fn parse(input: &str) -> Result<Vec<Block>, ParseError> {
67	let mut parser = Parser::new(input);
68	parser.parse_blocks()
69}
70
71#[cfg(test)]
72pub(crate) fn parse_command(input: &str) -> Result<Command, ParseError> {
73	let mut parser = Parser::new(input);
74	parser.parse_command()
75}
76
77struct Parser<'a> {
78	input: &'a str,
79	pos: usize,
80	line: u32,
81	column: usize,
82	line_start_pos: usize,
83}
84
85impl<'a> Parser<'a> {
86	fn new(input: &'a str) -> Self {
87		Parser {
88			input,
89			pos: 0,
90			line: 1,
91			column: 1,
92			line_start_pos: 0,
93		}
94	}
95
96	fn current_char(&self) -> Option<char> {
97		self.input[self.pos..].chars().next()
98	}
99
100	fn peek_char(&self) -> Option<char> {
101		self.current_char()
102	}
103
104	fn peek_str(&self, n: usize) -> &str {
105		let end = (self.pos + n).min(self.input.len());
106
107		let mut safe_end = end;
108		while safe_end > self.pos && !self.input.is_char_boundary(safe_end) {
109			safe_end -= 1;
110		}
111
112		&self.input[self.pos..safe_end]
113	}
114
115	fn advance(&mut self) -> Option<char> {
116		if let Some(ch) = self.current_char() {
117			self.pos += ch.len_utf8();
118			if ch == '\n' {
119				self.line += 1;
120				self.column = 1;
121				self.line_start_pos = self.pos;
122			} else {
123				self.column += 1;
124			}
125			Some(ch)
126		} else {
127			None
128		}
129	}
130
131	fn skip_whitespace(&mut self) {
132		while let Some(ch) = self.peek_char() {
133			if ch.is_whitespace() && ch != '\n' {
134				self.advance();
135			} else {
136				break;
137			}
138		}
139	}
140
141	fn skip_line(&mut self) {
142		while let Some(ch) = self.peek_char() {
143			if ch == '\n' {
144				self.advance();
145				break;
146			}
147			self.advance();
148		}
149	}
150
151	fn is_at_end(&self) -> bool {
152		self.pos >= self.input.len()
153	}
154
155	fn error(&self, message: impl Into<String>) -> ParseError {
156		let line_end = self.input[self.line_start_pos..]
157			.find('\n')
158			.map(|i| self.line_start_pos + i)
159			.unwrap_or(self.input.len());
160		let line_text = &self.input[self.line_start_pos..line_end];
161
162		ParseError {
163			message: message.into(),
164			line: self.line,
165			column: self.column,
166			input: LocatedSpan::new(self.line_start_pos, self.column, self.line, line_text.to_string()),
167			code: format!("{:?}", line_text),
168		}
169	}
170
171	fn parse_blocks(&mut self) -> Result<Vec<Block>, ParseError> {
172		let mut blocks = Vec::new();
173
174		while !self.is_at_end() {
175			if let Some(block) = self.parse_block()? {
176				blocks.push(block);
177			}
178		}
179
180		Ok(blocks)
181	}
182
183	fn parse_block(&mut self) -> Result<Option<Block>, ParseError> {
184		let line_number = self.line;
185		let literal_start = self.pos;
186
187		let commands = self.parse_commands()?;
188
189		let literal_end = self.pos;
190		let literal = self.input[literal_start..literal_end].to_string();
191
192		if self.is_at_end() && commands.is_empty() {
193			return Ok(Some(Block {
194				literal,
195				commands,
196				line_number,
197			}));
198		}
199
200		if commands.is_empty() {
201			return Ok(None);
202		}
203
204		if !self.parse_separator()? {
205			return Err(self.error("Expected --- separator"));
206		}
207
208		self.parse_output()?;
209
210		Ok(Some(Block {
211			literal,
212			commands,
213			line_number,
214		}))
215	}
216
217	fn parse_commands(&mut self) -> Result<Vec<Command>, ParseError> {
218		let mut commands = Vec::new();
219
220		loop {
221			if self.skip_empty_or_comment_line() {
222				continue;
223			}
224
225			if self.is_at_end() {
226				break;
227			}
228
229			if self.peek_str(3) == "---" && !commands.is_empty() {
230				break;
231			}
232
233			if let Some(ch) = self.peek_char()
234				&& ch.is_whitespace() && ch != '\n'
235			{
236				return Err(self.error("Command cannot start with whitespace"));
237			}
238
239			match self.parse_command() {
240				Ok(cmd) => commands.push(cmd),
241				Err(e) => {
242					if self.peek_str(3) == "---" && commands.is_empty() {
243						return Err(e);
244					}
245					return Err(e);
246				}
247			}
248		}
249
250		Ok(commands)
251	}
252
253	fn parse_command(&mut self) -> Result<Command, ParseError> {
254		let line_number = self.line;
255
256		let silent = if self.peek_char() == Some('(') {
257			self.advance();
258			self.skip_whitespace();
259			true
260		} else {
261			false
262		};
263
264		let mut tags = HashSet::new();
265		let mut prefix = None;
266
267		let saved_pos = self.pos;
268		self.skip_whitespace();
269		if let Ok(s) = self.parse_string() {
270			self.skip_whitespace();
271			if self.peek_char() == Some(':') {
272				self.advance();
273				self.skip_whitespace();
274				prefix = Some(s);
275			} else {
276				self.pos = saved_pos;
277			}
278		}
279
280		self.skip_whitespace();
281		if let Some(parsed_tags) = self.parse_taglist()? {
282			tags.extend(parsed_tags);
283		}
284		self.skip_whitespace();
285
286		let fail = if self.peek_char() == Some('!') {
287			self.advance();
288			self.skip_whitespace();
289			true
290		} else {
291			false
292		};
293
294		if self.peek_char() == Some('>') {
295			self.advance();
296			self.skip_whitespace();
297
298			let fail = if !fail && self.peek_char() == Some('!') {
299				self.advance();
300				self.skip_whitespace();
301				true
302			} else {
303				fail
304			};
305			let name = self.parse_line_continuation()?;
306			return Ok(Command {
307				name,
308				args: Vec::new(),
309				tags,
310				prefix,
311				silent,
312				fail,
313				line_number,
314			});
315		}
316
317		self.skip_whitespace();
318		let name = self.parse_string().map_err(|_| self.error("Expected command name"))?;
319
320		let mut args = Vec::new();
321		loop {
322			self.skip_whitespace();
323			if self.peek_char() == Some('[')
324				&& let Some(parsed_tags) = self.parse_taglist()?
325			{
326				tags.extend(parsed_tags);
327				break;
328			}
329
330			if silent && self.peek_char() == Some(')') {
331				break;
332			}
333
334			if self.peek_char() == Some('#') || self.peek_str(2) == "//" {
335				break;
336			}
337
338			if self.peek_char() == Some('\n') || self.is_at_end() {
339				break;
340			}
341
342			let saved_pos = self.pos;
343			let saved_line = self.line;
344			let saved_column = self.column;
345			let saved_line_start = self.line_start_pos;
346			match self.parse_argument() {
347				Ok(arg) => args.push(arg),
348				Err(_) => {
349					self.pos = saved_pos;
350					self.line = saved_line;
351					self.column = saved_column;
352					self.line_start_pos = saved_line_start;
353					break;
354				}
355			}
356		}
357
358		if silent {
359			self.skip_whitespace();
360			if self.peek_char() != Some(')') {
361				return Err(self.error("Expected closing ) for silent command"));
362			}
363			self.advance();
364		}
365
366		self.skip_whitespace();
367		if self.peek_char() == Some('#') || self.peek_str(2) == "//" {
368			self.skip_line();
369		} else if self.peek_char() == Some('\n') {
370			self.advance();
371		} else if !self.is_at_end() {
372			return Err(self.error("Expected end of line"));
373		}
374
375		Ok(Command {
376			name,
377			args,
378			tags,
379			prefix,
380			silent,
381			fail,
382			line_number,
383		})
384	}
385
386	fn parse_argument(&mut self) -> Result<Argument, ParseError> {
387		let saved_pos = self.pos;
388		let saved_line = self.line;
389		let saved_column = self.column;
390		let saved_line_start = self.line_start_pos;
391
392		self.skip_whitespace();
393		if let Ok(key) = self.parse_string() {
394			if self.peek_char() == Some('=') {
395				self.advance();
396
397				let value = if matches!(self.peek_char(), Some(ch) if ch.is_whitespace())
398					|| matches!(self.peek_char(), Some('[' | ')' | '#'))
399					|| self.peek_char().is_none() || self.peek_str(2) == "//"
400				{
401					String::new()
402				} else {
403					match self.parse_string() {
404						Ok(v) => v,
405						Err(_) => {
406							let check_pos = self.pos;
407							let check_line = self.line;
408							let check_column = self.column;
409							let check_line_start = self.line_start_pos;
410
411							if self.parse_string().is_ok() {
412								if self.peek_char() == Some('=') {
413									self.pos = check_pos;
414									self.line = check_line;
415									self.column = check_column;
416									self.line_start_pos = check_line_start;
417									String::new()
418								} else {
419									self.pos = saved_pos;
420									self.line = saved_line;
421									self.column = saved_column;
422									self.line_start_pos = saved_line_start;
423									return Err(self.error(
424										"Expected argument value after =",
425									));
426								}
427							} else {
428								self.pos = saved_pos;
429								self.line = saved_line;
430								self.column = saved_column;
431								self.line_start_pos = saved_line_start;
432								return Err(
433									self.error("Expected argument value after =")
434								);
435							}
436						}
437					}
438				};
439				return Ok(Argument {
440					key: Some(key),
441					value,
442				});
443			}
444
445			return Ok(Argument {
446				key: None,
447				value: key,
448			});
449		}
450
451		self.pos = saved_pos;
452		Err(self.error("Expected argument"))
453	}
454
455	fn parse_taglist(&mut self) -> Result<Option<HashSet<String>>, ParseError> {
456		if self.peek_char() != Some('[') {
457			return Ok(None);
458		}
459
460		self.advance();
461		let mut tags = HashSet::new();
462
463		loop {
464			self.skip_whitespace();
465
466			if self.peek_char() == Some(']') {
467				if tags.is_empty() {
468					return Err(self.error("Empty tag list"));
469				}
470				self.advance();
471				break;
472			}
473
474			self.skip_whitespace();
475			let tag = self.parse_string().map_err(|_| self.error("Expected tag name"))?;
476			tags.insert(tag);
477
478			self.skip_whitespace();
479			if self.peek_char() == Some(',') {
480				self.advance();
481				self.skip_whitespace();
482			} else if self.peek_char() == Some(' ') {
483				self.skip_whitespace();
484			}
485		}
486
487		Ok(Some(tags))
488	}
489
490	fn parse_string(&mut self) -> Result<String, ParseError> {
491		match self.peek_char() {
492			Some('\'') => self.parse_quoted_string('\''),
493			Some('"') => self.parse_quoted_string('"'),
494			_ => self.parse_unquoted_string(),
495		}
496	}
497
498	fn parse_unquoted_string(&mut self) -> Result<String, ParseError> {
499		let mut result = String::new();
500
501		match self.peek_char() {
502			Some(ch) if ch.is_alphanumeric() || ch == '_' => {
503				result.push(ch);
504				self.advance();
505			}
506			_ => return Err(self.error("Expected string")),
507		}
508
509		while let Some(ch) = self.peek_char() {
510			if ch.is_alphanumeric() || "_-./@".contains(ch) {
511				result.push(ch);
512				self.advance();
513			} else {
514				break;
515			}
516		}
517
518		Ok(result)
519	}
520
521	fn parse_quoted_string(&mut self, quote: char) -> Result<String, ParseError> {
522		let mut result = String::new();
523
524		if self.peek_char() != Some(quote) {
525			return Err(self.error(format!("Expected {} quote", quote)));
526		}
527		self.advance();
528
529		while let Some(ch) = self.peek_char() {
530			if ch == quote {
531				self.advance();
532				return Ok(result);
533			} else if ch == '\\' {
534				self.advance();
535				match self.peek_char() {
536					Some('\'') => {
537						result.push('\'');
538						self.advance();
539					}
540					Some('"') => {
541						result.push('"');
542						self.advance();
543					}
544					Some('\\') => {
545						result.push('\\');
546						self.advance();
547					}
548					Some('0') => {
549						result.push('\0');
550						self.advance();
551					}
552					Some('n') => {
553						result.push('\n');
554						self.advance();
555					}
556					Some('r') => {
557						result.push('\r');
558						self.advance();
559					}
560					Some('t') => {
561						result.push('\t');
562						self.advance();
563					}
564					Some('x') => {
565						self.advance();
566						let hex = self.parse_hex_digits(2, 2)?;
567						let byte = u8::from_str_radix(&hex, 16)
568							.map_err(|_| self.error("Invalid hex escape"))?;
569						result.push(char::from(byte));
570					}
571					Some('u') => {
572						self.advance();
573						if self.peek_char() != Some('{') {
574							return Err(self.error("Expected { after \\u"));
575						}
576						self.advance();
577						let hex = self.parse_hex_digits(1, 6)?;
578						if self.peek_char() != Some('}') {
579							return Err(self.error("Expected } after unicode escape"));
580						}
581						self.advance();
582						let codepoint = u32::from_str_radix(&hex, 16)
583							.map_err(|_| self.error("Invalid unicode escape"))?;
584						let ch = char::from_u32(codepoint)
585							.ok_or_else(|| self.error("Invalid unicode codepoint"))?;
586						result.push(ch);
587					}
588					_ => {
589						return Err(self.error("Invalid escape sequence"));
590					}
591				}
592			} else {
593				result.push(ch);
594				self.advance();
595			}
596		}
597
598		Err(self.error(format!("Unterminated string (missing {})", quote)))
599	}
600
601	fn parse_hex_digits(&mut self, min: usize, max: usize) -> Result<String, ParseError> {
602		let mut hex = String::new();
603		for i in 0..max {
604			match self.peek_char() {
605				Some(ch) if ch.is_ascii_hexdigit() => {
606					hex.push(ch);
607					self.advance();
608				}
609				_ => {
610					if i < min {
611						return Err(self.error(format!("Expected at least {} hex digits", min)));
612					}
613					break;
614				}
615			}
616		}
617		if hex.len() < min {
618			return Err(self.error(format!("Expected at least {} hex digits", min)));
619		}
620		Ok(hex)
621	}
622
623	fn skip_empty_or_comment_line(&mut self) -> bool {
624		let saved_pos = self.pos;
625
626		self.skip_whitespace();
627
628		if self.peek_char() == Some('#') || self.peek_str(2) == "//" {
629			self.skip_line();
630			return true;
631		}
632
633		if self.peek_char() == Some('\n') {
634			self.advance();
635			return true;
636		}
637
638		self.pos = saved_pos;
639		false
640	}
641
642	fn parse_separator(&mut self) -> Result<bool, ParseError> {
643		if self.peek_str(3) != "---" {
644			return Ok(false);
645		}
646
647		self.advance();
648		self.advance();
649		self.advance();
650
651		match self.peek_char() {
652			Some('\r') => {
653				self.advance();
654				if self.peek_char() == Some('\n') {
655					self.advance();
656				}
657				Ok(true)
658			}
659			Some('\n') => {
660				self.advance();
661				Ok(true)
662			}
663			None => Ok(true),
664			_ => Err(self.error("Separator must be followed by newline or EOF")),
665		}
666	}
667
668	fn parse_output(&mut self) -> Result<(), ParseError> {
669		if self.peek_char() == Some('\n') || self.is_at_end() {
670			if self.peek_char() == Some('\n') {
671				self.advance();
672			}
673			return Ok(());
674		}
675
676		let mut last_was_newline = false;
677		while !self.is_at_end() {
678			let ch = self.advance().unwrap();
679			if ch == '\n' {
680				if last_was_newline {
681					break;
682				}
683				last_was_newline = true;
684			} else {
685				last_was_newline = false;
686			}
687		}
688
689		Ok(())
690	}
691
692	fn parse_line_continuation(&mut self) -> Result<String, ParseError> {
693		let mut result = String::new();
694
695		loop {
696			while let Some(ch) = self.peek_char() {
697				if ch == '\n' {
698					break;
699				}
700				result.push(ch);
701				self.advance();
702			}
703
704			if result.ends_with('\\') {
705				result.pop();
706				if self.peek_char() == Some('\n') {
707					self.advance();
708					continue;
709				}
710			}
711
712			if self.peek_char() == Some('\n') {
713				self.advance();
714			}
715
716			break;
717		}
718
719		Ok(result)
720	}
721}