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		// This function returns n bytes from current position
106		// It's only used for checking ASCII patterns like "---" and
107		// "//"
108		let end = (self.pos + n).min(self.input.len());
109
110		// Make sure we don't split a UTF-8 character
111		let mut safe_end = end;
112		while safe_end > self.pos && !self.input.is_char_boundary(safe_end) {
113			safe_end -= 1;
114		}
115
116		&self.input[self.pos..safe_end]
117	}
118
119	fn advance(&mut self) -> Option<char> {
120		if let Some(ch) = self.current_char() {
121			self.pos += ch.len_utf8();
122			if ch == '\n' {
123				self.line += 1;
124				self.column = 1;
125				self.line_start_pos = self.pos;
126			} else {
127				self.column += 1;
128			}
129			Some(ch)
130		} else {
131			None
132		}
133	}
134
135	fn skip_whitespace(&mut self) {
136		while let Some(ch) = self.peek_char() {
137			if ch.is_whitespace() && ch != '\n' {
138				self.advance();
139			} else {
140				break;
141			}
142		}
143	}
144
145	fn skip_line(&mut self) {
146		while let Some(ch) = self.peek_char() {
147			if ch == '\n' {
148				self.advance();
149				break;
150			}
151			self.advance();
152		}
153	}
154
155	fn is_at_end(&self) -> bool {
156		self.pos >= self.input.len()
157	}
158
159	fn error(&self, message: impl Into<String>) -> ParseError {
160		let line_end = self.input[self.line_start_pos..]
161			.find('\n')
162			.map(|i| self.line_start_pos + i)
163			.unwrap_or(self.input.len());
164		let line_text = &self.input[self.line_start_pos..line_end];
165
166		ParseError {
167			message: message.into(),
168			line: self.line,
169			column: self.column,
170			input: LocatedSpan::new(self.line_start_pos, self.column, self.line, line_text.to_string()),
171			code: format!("{:?}", line_text),
172		}
173	}
174
175	fn parse_blocks(&mut self) -> Result<Vec<Block>, ParseError> {
176		let mut blocks = Vec::new();
177
178		while !self.is_at_end() {
179			if let Some(block) = self.parse_block()? {
180				blocks.push(block);
181			}
182		}
183
184		Ok(blocks)
185	}
186
187	fn parse_block(&mut self) -> Result<Option<Block>, ParseError> {
188		let line_number = self.line;
189		let literal_start = self.pos;
190
191		// Parse commands
192		let commands = self.parse_commands()?;
193
194		// Capture literal
195		let literal_end = self.pos;
196		let literal = self.input[literal_start..literal_end].to_string();
197
198		// Handle empty block at EOF
199		if self.is_at_end() && commands.is_empty() {
200			return Ok(Some(Block {
201				literal,
202				commands,
203				line_number,
204			}));
205		}
206
207		// If no commands and not at EOF, this isn't a valid block
208		if commands.is_empty() {
209			return Ok(None);
210		}
211
212		// Parse separator
213		if !self.parse_separator()? {
214			return Err(self.error("Expected --- separator"));
215		}
216
217		// Parse and skip output
218		self.parse_output()?;
219
220		Ok(Some(Block {
221			literal,
222			commands,
223			line_number,
224		}))
225	}
226
227	fn parse_commands(&mut self) -> Result<Vec<Command>, ParseError> {
228		let mut commands = Vec::new();
229
230		loop {
231			// Skip empty and comment lines
232			if self.skip_empty_or_comment_line() {
233				continue;
234			}
235
236			// Check for EOF
237			if self.is_at_end() {
238				break;
239			}
240
241			// Check for separator
242			if self.peek_str(3) == "---" && !commands.is_empty() {
243				break;
244			}
245
246			// Check for leading whitespace (not allowed for
247			// commands)
248			if let Some(ch) = self.peek_char()
249				&& ch.is_whitespace() && ch != '\n'
250			{
251				return Err(self.error("Command cannot start with whitespace"));
252			}
253
254			// Parse command
255			match self.parse_command() {
256				Ok(cmd) => commands.push(cmd),
257				Err(e) => {
258					// If we hit a separator but have no
259					// commands, let parse_command error
260					if self.peek_str(3) == "---" && commands.is_empty() {
261						return Err(e);
262					}
263					return Err(e);
264				}
265			}
266		}
267
268		Ok(commands)
269	}
270
271	fn parse_command(&mut self) -> Result<Command, ParseError> {
272		let line_number = self.line;
273
274		// Check for silencing (
275		let silent = if self.peek_char() == Some('(') {
276			self.advance();
277			self.skip_whitespace();
278			true
279		} else {
280			false
281		};
282
283		// Parse prefix and tags
284		let mut tags = HashSet::new();
285		let mut prefix = None;
286
287		// Try to parse prefix (string followed by :)
288		let saved_pos = self.pos;
289		self.skip_whitespace();
290		if let Ok(s) = self.parse_string() {
291			self.skip_whitespace();
292			if self.peek_char() == Some(':') {
293				self.advance();
294				self.skip_whitespace();
295				prefix = Some(s);
296			} else {
297				// Backtrack
298				self.pos = saved_pos;
299			}
300		}
301
302		// Parse tags before command
303		self.skip_whitespace();
304		if let Some(parsed_tags) = self.parse_taglist()? {
305			tags.extend(parsed_tags);
306		}
307		self.skip_whitespace();
308
309		// Check for fail marker
310		let fail = if self.peek_char() == Some('!') {
311			self.advance();
312			self.skip_whitespace();
313			true
314		} else {
315			false
316		};
317
318		// Check for literal command (>)
319		if self.peek_char() == Some('>') {
320			self.advance();
321			self.skip_whitespace();
322			// Also support >! (fail marker after literal marker)
323			let fail = if !fail && self.peek_char() == Some('!') {
324				self.advance();
325				self.skip_whitespace();
326				true
327			} else {
328				fail
329			};
330			let name = self.parse_line_continuation()?;
331			return Ok(Command {
332				name,
333				args: Vec::new(),
334				tags,
335				prefix,
336				silent,
337				fail,
338				line_number,
339			});
340		}
341
342		// Parse command name
343		self.skip_whitespace();
344		let name = self.parse_string().map_err(|_| self.error("Expected command name"))?;
345
346		// Parse arguments
347		let mut args = Vec::new();
348		loop {
349			self.skip_whitespace();
350			if self.peek_char() == Some('[') {
351				// Might be trailing tags
352				if let Some(parsed_tags) = self.parse_taglist()? {
353					tags.extend(parsed_tags);
354					break;
355				}
356			}
357
358			// Check for end of command
359			if silent && self.peek_char() == Some(')') {
360				break;
361			}
362
363			if self.peek_char() == Some('#') || self.peek_str(2) == "//" {
364				break;
365			}
366
367			if self.peek_char() == Some('\n') || self.is_at_end() {
368				break;
369			}
370
371			// Try to parse an argument
372			let saved_pos = self.pos;
373			let saved_line = self.line;
374			let saved_column = self.column;
375			let saved_line_start = self.line_start_pos;
376			match self.parse_argument() {
377				Ok(arg) => args.push(arg),
378				Err(_) => {
379					self.pos = saved_pos;
380					self.line = saved_line;
381					self.column = saved_column;
382					self.line_start_pos = saved_line_start;
383					break;
384				}
385			}
386		}
387
388		// Handle closing ) for silent commands
389		if silent {
390			self.skip_whitespace();
391			if self.peek_char() != Some(')') {
392				return Err(self.error("Expected closing ) for silent command"));
393			}
394			self.advance();
395		}
396
397		// Skip trailing whitespace and comments
398		self.skip_whitespace();
399		if self.peek_char() == Some('#') || self.peek_str(2) == "//" {
400			self.skip_line();
401		} else if self.peek_char() == Some('\n') {
402			self.advance();
403		} else if !self.is_at_end() {
404			return Err(self.error("Expected end of line"));
405		}
406
407		Ok(Command {
408			name,
409			args,
410			tags,
411			prefix,
412			silent,
413			fail,
414			line_number,
415		})
416	}
417
418	fn parse_argument(&mut self) -> Result<Argument, ParseError> {
419		// Try key=value format first
420		let saved_pos = self.pos;
421		let saved_line = self.line;
422		let saved_column = self.column;
423		let saved_line_start = self.line_start_pos;
424
425		self.skip_whitespace();
426		if let Ok(key) = self.parse_string() {
427			if self.peek_char() == Some('=') {
428				self.advance();
429				// Allow empty value after =
430				// First check if we have an empty value (next
431				// is whitespace or another key)
432				let value = if matches!(self.peek_char(), Some(ch) if ch.is_whitespace())
433					|| matches!(self.peek_char(), Some('[' | ')' | '#'))
434					|| self.peek_char().is_none() || self.peek_str(2) == "//"
435				{
436					// Empty value case
437					String::new()
438				} else {
439					// Try to parse a value
440					match self.parse_string() {
441						Ok(v) => v,
442						Err(_) => {
443							// Check if this looks
444							// like another
445							// key=value pair
446							// by looking for a
447							// string followed by =
448							let check_pos = self.pos;
449							let check_line = self.line;
450							let check_column = self.column;
451							let check_line_start = self.line_start_pos;
452
453							// Try to parse what
454							// comes next as a
455							// potential key
456							if self.parse_string().is_ok() {
457								if self.peek_char() == Some('=') {
458									// This looks like another key=value, so current
459									// value is empty
460									self.pos = check_pos;
461									self.line = check_line;
462									self.column = check_column;
463									self.line_start_pos = check_line_start;
464									String::new()
465								} else {
466									// Not a key=value, this is an error
467									self.pos = saved_pos;
468									self.line = saved_line;
469									self.column = saved_column;
470									self.line_start_pos = saved_line_start;
471									return Err(self.error(
472										"Expected argument value after =",
473									));
474								}
475							} else {
476								// Can't parse
477								// anything, this
478								// is an error
479								self.pos = saved_pos;
480								self.line = saved_line;
481								self.column = saved_column;
482								self.line_start_pos = saved_line_start;
483								return Err(
484									self.error("Expected argument value after =")
485								);
486							}
487						}
488					}
489				};
490				return Ok(Argument {
491					key: Some(key),
492					value,
493				});
494			}
495			// Just a value
496			return Ok(Argument {
497				key: None,
498				value: key,
499			});
500		}
501
502		self.pos = saved_pos;
503		Err(self.error("Expected argument"))
504	}
505
506	fn parse_taglist(&mut self) -> Result<Option<HashSet<String>>, ParseError> {
507		if self.peek_char() != Some('[') {
508			return Ok(None);
509		}
510
511		self.advance();
512		let mut tags = HashSet::new();
513
514		loop {
515			self.skip_whitespace();
516
517			if self.peek_char() == Some(']') {
518				// Empty tag list is an error
519				if tags.is_empty() {
520					return Err(self.error("Empty tag list"));
521				}
522				self.advance();
523				break;
524			}
525
526			self.skip_whitespace();
527			let tag = self.parse_string().map_err(|_| self.error("Expected tag name"))?;
528			tags.insert(tag);
529
530			self.skip_whitespace();
531			if self.peek_char() == Some(',') {
532				self.advance();
533				self.skip_whitespace();
534			} else if self.peek_char() == Some(' ') {
535				self.skip_whitespace();
536			}
537		}
538
539		Ok(Some(tags))
540	}
541
542	fn parse_string(&mut self) -> Result<String, ParseError> {
543		// Note: Don't skip whitespace here - the caller should handle
544		// that self.skip_whitespace();
545
546		match self.peek_char() {
547			Some('\'') => self.parse_quoted_string('\''),
548			Some('"') => self.parse_quoted_string('"'),
549			_ => self.parse_unquoted_string(),
550		}
551	}
552
553	fn parse_unquoted_string(&mut self) -> Result<String, ParseError> {
554		let mut result = String::new();
555
556		// First character must be alphanumeric or _
557		match self.peek_char() {
558			Some(ch) if ch.is_alphanumeric() || ch == '_' => {
559				result.push(ch);
560				self.advance();
561			}
562			_ => return Err(self.error("Expected string")),
563		}
564
565		// Subsequent characters
566		while let Some(ch) = self.peek_char() {
567			if ch.is_alphanumeric() || "_-./@".contains(ch) {
568				result.push(ch);
569				self.advance();
570			} else {
571				break;
572			}
573		}
574
575		Ok(result)
576	}
577
578	fn parse_quoted_string(&mut self, quote: char) -> Result<String, ParseError> {
579		let mut result = String::new();
580
581		// Skip opening quote
582		if self.peek_char() != Some(quote) {
583			return Err(self.error(format!("Expected {} quote", quote)));
584		}
585		self.advance();
586
587		while let Some(ch) = self.peek_char() {
588			if ch == quote {
589				self.advance();
590				return Ok(result);
591			} else if ch == '\\' {
592				self.advance();
593				match self.peek_char() {
594					Some('\'') => {
595						result.push('\'');
596						self.advance();
597					}
598					Some('"') => {
599						result.push('"');
600						self.advance();
601					}
602					Some('\\') => {
603						result.push('\\');
604						self.advance();
605					}
606					Some('0') => {
607						result.push('\0');
608						self.advance();
609					}
610					Some('n') => {
611						result.push('\n');
612						self.advance();
613					}
614					Some('r') => {
615						result.push('\r');
616						self.advance();
617					}
618					Some('t') => {
619						result.push('\t');
620						self.advance();
621					}
622					Some('x') => {
623						self.advance();
624						let hex = self.parse_hex_digits(2, 2)?;
625						let byte = u8::from_str_radix(&hex, 16)
626							.map_err(|_| self.error("Invalid hex escape"))?;
627						result.push(char::from(byte));
628					}
629					Some('u') => {
630						self.advance();
631						if self.peek_char() != Some('{') {
632							return Err(self.error("Expected { after \\u"));
633						}
634						self.advance();
635						let hex = self.parse_hex_digits(1, 6)?;
636						if self.peek_char() != Some('}') {
637							return Err(self.error("Expected } after unicode escape"));
638						}
639						self.advance();
640						let codepoint = u32::from_str_radix(&hex, 16)
641							.map_err(|_| self.error("Invalid unicode escape"))?;
642						let ch = char::from_u32(codepoint)
643							.ok_or_else(|| self.error("Invalid unicode codepoint"))?;
644						result.push(ch);
645					}
646					_ => {
647						return Err(self.error("Invalid escape sequence"));
648					}
649				}
650			} else {
651				result.push(ch);
652				self.advance();
653			}
654		}
655
656		Err(self.error(format!("Unterminated string (missing {})", quote)))
657	}
658
659	fn parse_hex_digits(&mut self, min: usize, max: usize) -> Result<String, ParseError> {
660		let mut hex = String::new();
661		for i in 0..max {
662			match self.peek_char() {
663				Some(ch) if ch.is_ascii_hexdigit() => {
664					hex.push(ch);
665					self.advance();
666				}
667				_ => {
668					if i < min {
669						return Err(self.error(format!("Expected at least {} hex digits", min)));
670					}
671					break;
672				}
673			}
674		}
675		if hex.len() < min {
676			return Err(self.error(format!("Expected at least {} hex digits", min)));
677		}
678		Ok(hex)
679	}
680
681	fn skip_empty_or_comment_line(&mut self) -> bool {
682		let saved_pos = self.pos;
683
684		self.skip_whitespace();
685
686		// Check for comment
687		if self.peek_char() == Some('#') || self.peek_str(2) == "//" {
688			self.skip_line();
689			return true;
690		}
691
692		// Check for empty line
693		if self.peek_char() == Some('\n') {
694			self.advance();
695			return true;
696		}
697
698		// Not an empty or comment line, restore position
699		self.pos = saved_pos;
700		false
701	}
702
703	fn parse_separator(&mut self) -> Result<bool, ParseError> {
704		if self.peek_str(3) != "---" {
705			return Ok(false);
706		}
707
708		self.advance(); // -
709		self.advance(); // -
710		self.advance(); // -
711
712		// Must be followed by newline (with optional \r) or EOF
713		match self.peek_char() {
714			Some('\r') => {
715				self.advance();
716				if self.peek_char() == Some('\n') {
717					self.advance();
718				}
719				Ok(true)
720			}
721			Some('\n') => {
722				self.advance();
723				Ok(true)
724			}
725			None => Ok(true),
726			_ => Err(self.error("Separator must be followed by newline or EOF")),
727		}
728	}
729
730	fn parse_output(&mut self) -> Result<(), ParseError> {
731		// Special case: no output (immediate newline or EOF)
732		if self.peek_char() == Some('\n') || self.is_at_end() {
733			if self.peek_char() == Some('\n') {
734				self.advance();
735			}
736			return Ok(());
737		}
738
739		// Read until double newline or EOF
740		let mut last_was_newline = false;
741		while !self.is_at_end() {
742			let ch = self.advance().unwrap();
743			if ch == '\n' {
744				if last_was_newline {
745					break;
746				}
747				last_was_newline = true;
748			} else {
749				last_was_newline = false;
750			}
751		}
752
753		Ok(())
754	}
755
756	fn parse_line_continuation(&mut self) -> Result<String, ParseError> {
757		let mut result = String::new();
758
759		loop {
760			// Read until end of line
761			while let Some(ch) = self.peek_char() {
762				if ch == '\n' {
763					break;
764				}
765				result.push(ch);
766				self.advance();
767			}
768
769			// Check for continuation
770			if result.ends_with('\\') {
771				result.pop(); // Remove the backslash
772				if self.peek_char() == Some('\n') {
773					self.advance(); // Skip newline
774					continue;
775				}
776			}
777
778			// Skip the final newline
779			if self.peek_char() == Some('\n') {
780				self.advance();
781			}
782
783			break;
784		}
785
786		Ok(result)
787	}
788}