reifydb-rql 0.9.1

ReifyDB Query Language (RQL) parser and AST
Documentation
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2026 ReifyDB

use crate::{
	Result,
	ast::{
		ast::{Ast, AstCall, AstCallFunction, AstList, AstLiteral, AstLiteralText, AstTuple},
		identifier::MaybeQualifiedFunctionIdentifier,
		parse::{Parser, Precedence},
	},
	bump::BumpFragment,
	token::{
		keyword::Keyword,
		operator::Operator,
		separator::Separator::Comma,
		token::{Literal, Token, TokenKind},
	},
};

impl<'bump> Parser<'bump> {
	pub(crate) fn parse_call(&mut self) -> Result<AstCall<'bump>> {
		let token = self.consume_keyword(Keyword::Call)?;

		let first_ident = self.consume_identifier()?;
		let mut namespace_fragments = Vec::new();

		let mut current = first_ident;
		while self.current()?.is_operator(Operator::DoubleColon) {
			namespace_fragments.push(current.fragment);
			self.advance()?;
			current = self.consume_identifier()?;
		}

		let function = if namespace_fragments.is_empty() {
			MaybeQualifiedFunctionIdentifier::new(current.fragment)
		} else {
			MaybeQualifiedFunctionIdentifier::new(current.fragment).with_namespaces(namespace_fragments)
		};

		let arguments = self.parse_call_arguments()?;

		Ok(AstCall {
			token,
			function,
			arguments,
		})
	}

	fn parse_call_arguments(&mut self) -> Result<AstTuple<'bump>> {
		let token = self.consume_operator(Operator::OpenParen)?;

		let mut nodes = Vec::with_capacity(4);
		loop {
			self.skip_new_line()?;

			if self.current()?.is_operator(Operator::CloseParen) {
				break;
			}
			nodes.push(self.parse_call_argument()?);
			if self.consume_if(TokenKind::Separator(Comma))?.is_none() {
				break;
			};
		}

		self.consume_operator(Operator::CloseParen)?;
		Ok(AstTuple {
			token,
			nodes,
		})
	}

	fn parse_call_argument(&mut self) -> Result<Ast<'bump>> {
		if self.current()?.is_operator(Operator::OpenBracket) {
			let token = self.consume_operator(Operator::OpenBracket)?;

			let mut nodes = Vec::with_capacity(4);
			loop {
				self.skip_new_line()?;

				if self.is_eof() || self.current()?.is_operator(Operator::CloseBracket) {
					break;
				}
				nodes.push(self.parse_call_argument()?);
				if self.consume_if(TokenKind::Separator(Comma))?.is_none() {
					break;
				};
			}

			self.consume_operator(Operator::CloseBracket)?;
			return Ok(Ast::List(AstList {
				token,
				nodes,
			}));
		}

		if self.current()?.is_identifier() && !self.is_function_call_pattern() {
			return self.parse_qualified_name_argument();
		}

		self.parse_node(Precedence::None)
	}

	fn parse_qualified_name_argument(&mut self) -> Result<Ast<'bump>> {
		let start = self.current()?.fragment;
		let line = start.line();
		let column = start.column();

		let segments = self.parse_double_colon_separated_identifiers()?;
		let mut joined = String::new();
		for segment in &segments {
			if !joined.is_empty() {
				joined.push_str("::");
			}
			joined.push_str(segment.text());
		}

		let text = self.bump().alloc_str(&joined);
		let fragment = BumpFragment::Statement {
			text,
			offset: 0,
			source_end: 0,
			line,
			column,
		};

		Ok(Ast::Literal(AstLiteral::Text(AstLiteralText(Token {
			kind: TokenKind::Literal(Literal::Text),
			fragment,
		}))))
	}

	pub(crate) fn parse_function_call(&mut self) -> Result<AstCallFunction<'bump>> {
		let first_ident_token = self.consume_identifier()?;
		let start_token = first_ident_token;

		if self.current()?.is_operator(Operator::OpenParen) {
			let open_paren_token = self.advance()?;

			let arguments = self.parse_tuple_call(open_paren_token)?;

			let function = MaybeQualifiedFunctionIdentifier::new(first_ident_token.fragment);

			return Ok(AstCallFunction {
				token: start_token,
				function,
				arguments,
			});
		}

		let mut current_ident_token = first_ident_token;
		let mut namespace_fragments = Vec::new();

		while self.current()?.is_operator(Operator::DoubleColon) {
			namespace_fragments.push(current_ident_token.fragment);

			self.advance()?;
			let next_ident_token = self.consume_identifier()?;

			if self.current()?.is_operator(Operator::OpenParen) {
				let open_paren_token = self.advance()?;

				let arguments = self.parse_tuple_call(open_paren_token)?;

				let function = MaybeQualifiedFunctionIdentifier::new(next_ident_token.fragment)
					.with_namespaces(namespace_fragments);

				return Ok(AstCallFunction {
					token: start_token,
					function,
					arguments,
				});
			} else {
				current_ident_token = next_ident_token;
			}
		}

		unreachable!("parse_function_call called on non-function call pattern")
	}

	pub(crate) fn is_function_call_pattern(&self) -> bool {
		let tokens_len = self.tokens.len();
		if self.position >= tokens_len {
			return false;
		}

		// SAFETY: `self.position < tokens_len` was checked immediately above.
		if !unsafe { self.tokens.get_unchecked(self.position) }.is_identifier() {
			return false;
		}

		if self.position + 1 < tokens_len
			// SAFETY: the short-circuiting left operand established `self.position + 1 < tokens_len`.
			&& unsafe { self.tokens.get_unchecked(self.position + 1) }.is_operator(Operator::OpenParen)
		{
			return true;
		}

		let mut pos = self.position + 1;
		while pos + 2 < tokens_len {
			// SAFETY: the loop condition `pos + 2 < tokens_len` implies `pos < tokens_len`.
			if !unsafe { self.tokens.get_unchecked(pos) }.is_operator(Operator::DoubleColon) {
				return false;
			}
			pos += 1;

			// SAFETY: the loop head established `pos + 2 < tokens_len`; after one increment
			// `pos + 1 < tokens_len`, so `pos` is in bounds.
			let token = unsafe { self.tokens.get_unchecked(pos) };
			if !token.is_identifier() && !token.is_keyword_as_ident() {
				return false;
			}
			pos += 1;

			// SAFETY: the loop head established `pos + 2 < tokens_len`; after two increments
			// `pos < tokens_len` still holds.
			if unsafe { self.tokens.get_unchecked(pos) }.is_operator(Operator::OpenParen) {
				return true;
			}
		}

		false
	}
}

#[cfg(test)]
pub mod tests {
	use bumpalo::Bump;

	use crate::{
		ast::{ast::Ast, parse::parse},
		token::tokenize,
	};

	#[test]
	fn test_namespaced_call() {
		let bump = Bump::new();
		let source = "CALL ns::greet()";
		let tokens = tokenize(&bump, source).unwrap().into_iter().collect();
		let result = parse(&bump, source, tokens).unwrap();
		assert_eq!(result.len(), 1);
		let Ast::Call(call) = result[0].first_unchecked() else {
			panic!("expected Call")
		};
		assert_eq!(call.function.name.text(), "greet");
		assert_eq!(call.function.namespaces.len(), 1);
		assert_eq!(call.function.namespaces[0].text(), "ns");
	}

	#[test]
	fn test_simple_call() {
		let bump = Bump::new();
		let source = "CALL greet()";
		let tokens = tokenize(&bump, source).unwrap().into_iter().collect();
		let result = parse(&bump, source, tokens).unwrap();
		assert_eq!(result.len(), 1);
		let Ast::Call(call) = result[0].first_unchecked() else {
			panic!("expected Call")
		};
		assert_eq!(call.function.name.text(), "greet");
		assert!(call.function.namespaces.is_empty());
	}

	#[test]
	fn test_simple_function_call() {
		let bump = Bump::new();
		let source = "func()";
		let tokens = tokenize(&bump, source).unwrap().into_iter().collect();
		let result = parse(&bump, source, tokens).unwrap();
		assert_eq!(result.len(), 1);

		let call = result[0].first_unchecked().as_call_function();
		assert_eq!(call.function.name.text(), "func");
		assert!(call.function.namespaces.is_empty());
		assert_eq!(call.arguments.len(), 0);
	}

	#[test]
	fn test_function_call_with_args() {
		let bump = Bump::new();
		let source = "func('arg')";
		let tokens = tokenize(&bump, source).unwrap().into_iter().collect();
		let result = parse(&bump, source, tokens).unwrap();
		assert_eq!(result.len(), 1);

		let call = result[0].first_unchecked().as_call_function();
		assert_eq!(call.function.name.text(), "func");
		assert!(call.function.namespaces.is_empty());
		assert_eq!(call.arguments.len(), 1);
	}

	#[test]
	fn test_namespaced_function_call() {
		let bump = Bump::new();
		let source = "blob::hex('deadbeef')";
		let tokens = tokenize(&bump, source).unwrap().into_iter().collect();
		let result = parse(&bump, source, tokens).unwrap();
		assert_eq!(result.len(), 1);

		let call = result[0].first_unchecked().as_call_function();
		assert_eq!(call.function.name.text(), "hex");
		assert_eq!(call.function.namespaces.len(), 1);
		assert_eq!(call.function.namespaces[0].text(), "blob");
		assert_eq!(call.arguments.len(), 1);
	}

	#[test]
	fn test_deeply_nested_function_call() {
		let bump = Bump::new();
		let source = "ext::crypto::hash::sha256('data')";
		let tokens = tokenize(&bump, source).unwrap().into_iter().collect();
		let result = parse(&bump, source, tokens).unwrap();
		assert_eq!(result.len(), 1);

		let call = result[0].first_unchecked().as_call_function();
		assert_eq!(call.function.name.text(), "sha256");
		assert_eq!(call.function.namespaces.len(), 3);
		assert_eq!(call.function.namespaces[0].text(), "ext");
		assert_eq!(call.function.namespaces[1].text(), "crypto");
		assert_eq!(call.function.namespaces[2].text(), "hash");
		assert_eq!(call.arguments.len(), 1);
	}

	#[test]
	fn test_identifier_without_parens_not_function_call() {
		let bump = Bump::new();
		let source = "identifier";
		let tokens = tokenize(&bump, source).unwrap().into_iter().collect();
		let result = parse(&bump, source, tokens).unwrap();
		assert_eq!(result.len(), 1);

		assert!(result[0].first_unchecked().as_identifier().text() == "identifier");
	}

	#[test]
	fn test_namespaced_function_call_with_keyword_name() {
		let bump = Bump::new();
		let source = "clock::set(1000)";
		let tokens = tokenize(&bump, source).unwrap().into_iter().collect();
		let result = parse(&bump, source, tokens).unwrap();
		assert_eq!(result.len(), 1);

		let call = result[0].first_unchecked().as_call_function();
		assert_eq!(call.function.name.text(), "set");
		assert_eq!(call.function.namespaces.len(), 1);
		assert_eq!(call.function.namespaces[0].text(), "clock");
	}

	#[test]
	fn test_namespace_access_without_parens_not_function_call() {
		let bump = Bump::new();
		let source = "namespace::identifier";
		let tokens = tokenize(&bump, source).unwrap().into_iter().collect();
		let result = parse(&bump, source, tokens).unwrap();
		assert_eq!(result.len(), 1);

		let infix = result[0].first_unchecked().as_infix();
		assert_eq!(infix.left.as_identifier().text(), "namespace");
		assert_eq!(infix.right.as_identifier().text(), "identifier");
	}
}