Skip to main content

reifydb_testing/testscript/
parser.rs

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