You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

425 lines
14 KiB
Rust

use std::borrow::Cow;
use std::fmt::Debug;
#[cfg(feature = "miette")]
use miette::{LabeledSpan, SourceCode};
use crate::lexer;
use crate::lexer::{Lexer, NoTracker, Token, Tracker};
use crate::span::Span;
#[cfg(feature = "miette")]
use crate::span::SpanOffset;
pub struct Parser<'a, T: Tracker = NoTracker> {
exclude_comments: bool,
lexer: Lexer<'a, T>,
}
#[derive(Debug, Copy, Clone)]
pub struct ParserOptions {
pub exclude_comments: bool,
}
impl ParserOptions {
pub fn new() -> Self {
ParserOptions {
exclude_comments: true,
}
}
pub fn include_comments(mut self) -> Self {
self.exclude_comments = false;
self
}
pub fn exclude_comments(mut self) -> Self {
self.exclude_comments = true;
self
}
pub fn build(self, input: &str) -> Parser {
self.build_with_tracker::<NoTracker>(input)
}
pub fn build_with_tracker<T: Tracker + Default>(self, input: &str) -> Parser<T> {
Parser::with_options::<T>(input, self)
}
}
impl Default for ParserOptions {
#[inline]
fn default() -> Self {
Self::new()
}
}
impl<'a> Parser<'a> {
pub fn new(input: &'a str) -> Parser<'a> {
Parser::with_tracker::<NoTracker>(input)
}
pub fn with_options<T: Tracker + Default>(input: &'a str, options: ParserOptions) -> Parser<'a, T> {
Parser {
exclude_comments: options.exclude_comments,
lexer: Lexer::with_tracker::<T>(input),
}
}
pub fn with_tracker<T: Tracker + Default>(input: &'a str) -> Parser<'a, T> {
Parser::with_options(input, Default::default())
}
}
type NodeSpan<'a, Offset> = Span<Node<'a, Offset>, Offset>;
enum NextResult<'a, O> {
None,
CloseGroup(Span<Token<'a>, O>),
Node(NodeSpan<'a, O>),
}
#[derive(Debug, Eq, PartialEq, thiserror::Error)]
pub enum Error<O: Debug> {
#[error("unsupported escape character '{character}'")]
UnsupportedEscape {
#[cfg(feature = "miette")]
input: String,
offset: O,
character: char,
span: Span<String, O>,
},
#[error("missing end quote")]
MissingEndQuote {
#[cfg(feature = "miette")]
input: String,
offset: O,
span: Span<String, O>,
},
#[error("missing closing bracket for group")]
MissingClosingBracket {
#[cfg(feature = "miette")]
input: String,
start_offset: O,
},
#[error("dangling close comment token")]
DanglingCloseComment {
#[cfg(feature = "miette")]
input: String,
offset: O,
span: Span<(), O>,
},
#[error("dangling close group paren")]
DanglingCloseParen {
#[cfg(feature = "miette")]
input: String,
offset: O,
span: Span<(), O>,
},
}
#[cfg(feature = "miette")]
impl<'a, O: SpanOffset + Debug> miette::Diagnostic for Error<O> {
fn source_code(&self) -> Option<&dyn SourceCode> {
match self {
Error::UnsupportedEscape { input, .. } |
Error::MissingEndQuote { input, .. } |
Error::MissingClosingBracket { input, .. } |
Error::DanglingCloseComment { input, .. } |
Error::DanglingCloseParen { input, .. } => Some(input)
}
}
fn labels(&self) -> Option<Box<dyn Iterator<Item=LabeledSpan> + '_>> {
let mut data = vec![];
match self {
Error::UnsupportedEscape { character, offset, span, .. } => {
data.push(LabeledSpan::new_primary_with_span(Some("here".to_string()), (offset.absolute_offset(), character.len_utf8())));
data.push(LabeledSpan::new_with_span(Some("in this string".to_string()), span));
}
Error::MissingEndQuote { span, .. } => {
data.push(LabeledSpan::new_primary_with_span(None, span));
}
Error::DanglingCloseComment { span, .. } | Error::DanglingCloseParen { span, .. } => {
data.push(LabeledSpan::new_primary_with_span(None, span));
}
Error::MissingClosingBracket { start_offset, .. } => {
data.push(LabeledSpan::new_primary_with_span(Some("group started here".to_string()), (start_offset.absolute_offset(), 1)));
}
}
Some(Box::new(data.into_iter()))
}
}
impl<'a, O: Debug> From<lexer::Error<'a, O>> for Error<O> {
fn from(value: lexer::Error<'a, O>) -> Self {
match value {
lexer::Error::UnsupportedEscape {
input: _input, offset, character, span
} => Error::UnsupportedEscape {
#[cfg(feature = "miette")]
input: _input.to_string(),
offset,
character,
span: span.into_string(),
},
lexer::Error::MissingEndQuote { input: _input, offset, span } => Error::MissingEndQuote {
#[cfg(feature = "miette")]
input: _input.to_string(),
offset,
span: span.into_string(),
}
}
}
}
impl<'a, T: Tracker> Parser<'a, T> {
fn inner_next(&mut self) -> Result<NextResult<'a, T::Offset>, Error<T::Offset>> {
let mut start: T::Offset;
'top: while let Some(token) = self.lexer.next() {
let token = token?;
start = token.start;
match token.value {
Token::LParen => {
let mut items = vec![];
loop {
match self.inner_next()? {
NextResult::None => {
return Err(Error::MissingClosingBracket {
#[cfg(feature = "miette")]
input: self.lexer.input.to_string(),
start_offset: start,
});
}
NextResult::Node(node) => {
items.push(node)
}
NextResult::CloseGroup(span) => {
return Ok(NextResult::Node(Span::new(start, span.end, Node::Group(items))));
}
}
}
}
Token::RParen => {
return Ok(NextResult::CloseGroup(token));
}
Token::LComment => {
let mut c_depth = 1;
while let Some(token) = self.lexer.next() {
let token = token?;
match token.value {
Token::LComment => {
c_depth += 1;
}
Token::RComment => {
c_depth -= 1;
if c_depth == 0 {
if self.exclude_comments {
continue 'top;
}
return Ok(NextResult::Node(Span::new(start, token.end, Node::Comment)));
}
}
_ => {}
}
}
}
Token::RComment => {
return Err(Error::DanglingCloseComment {
#[cfg(feature = "miette")]
input: self.lexer.input.to_string(),
offset: start,
span: Span::new(token.start, token.end, ()),
});
}
Token::String(str) => {
return Ok(NextResult::Node(Span::new(token.start, token.end, Node::String(str))));
}
Token::Atom(str) => {
return Ok(NextResult::Node(Span::new(token.start, token.end, Node::Atom(str))));
}
}
}
Ok(NextResult::None)
}
pub fn next(&mut self) -> Option<Result<NodeSpan<T::Offset>, Error<T::Offset>>> {
match self.inner_next() {
Err(e) => Some(Err(e)),
Ok(NextResult::None) => None,
Ok(NextResult::CloseGroup(span)) => Some(Err(Error::DanglingCloseParen {
#[cfg(feature = "miette")]
input: self.lexer.input.to_string(),
offset: span.start,
span: span.empty(),
})),
Ok(NextResult::Node(node)) => Some(Ok(node)),
}
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum Node<'a, Offset = ()> {
Group(Vec<Span<Node<'a, Offset>, Offset>>),
Atom(&'a str),
String(Cow<'a, str>),
Comment,
}
#[cfg(test)]
mod tests {
use std::borrow::Cow;
use crate::lexer::{LineTracker, OffsetTracker};
use crate::parser::{Error, Node, Parser, ParserOptions};
use crate::span::Span;
#[test]
pub fn test_simple() {
let mut parser = Parser::new(":");
assert_eq!(parser.next().unwrap().unwrap().value, Node::Atom(":"));
assert!(parser.next().is_none());
let mut parser = Parser::new(r#"(:next "wow")"#);
let Some(Ok(Span { value: Node::Group(items), .. })) = parser.next() else { panic!() };
assert_eq!(items.len(), 2);
assert_eq!(items[0].value, Node::Atom(":next"));
assert_eq!(items[1].value, Node::String(Cow::Borrowed("wow")));
}
#[test]
pub fn test_example() {
let example = r#"(service "dbus"
(env "DBUS_SESSION_BUS_ADDRESS" :export)
(exec dbus-daemon --nofork --session ("--print-address=" (fd :env "DBUS_SESSION_BUS_ADDRESS")))
(layer interactive)
)
(service "ssh-agent"
(env "SSH_AUTH_SOCK" :export (create-socket))
(exec ssh-agent -D -a (env "SSH_AUTH_SOCK"))
(layer interactive)
)
(service "pipewire"
(exec pipewire)
(needs (:after dbus))
)
(service "pipewire-pulse"
(exec pipewire-pulse)
(needs (:after pipewire))
)
(service "wireplumber"
(exec wireplumber)
(needs (:after pipewire))
)"#;
let mut parser = Parser::with_tracker::<LineTracker>(example);
while let Some(_) = parser.next() {}
}
#[test]
fn test_ignore_comments() {
let mut parser = Parser::new("#| #| hello! |# |# : #| bye! |#");
assert_eq!(parser.next().unwrap().unwrap().value, Node::Atom(":"));
assert!(parser.next().is_none());
let mut parser = Parser::new(r#"(:next #| hello! |# "wow")"#);
let Some(Ok(Span { value, .. })) = parser.next() else { panic!() };
format!("{:?}", value);
let Node::Group(items) = value else { panic!() };
assert_eq!(items.len(), 2);
assert_eq!(items[0].value, Node::Atom(":next"));
assert_eq!(items[1].value, Node::String(Cow::Borrowed("wow")));
let mut parser = ParserOptions::new().include_comments().build("#| hello |#");
let item = parser.next();
assert_eq!(item, Some(Ok(Span::new((), (), Node::Comment))));
let mut parser = ParserOptions::new().include_comments().exclude_comments().build("#| hello |#");
let item = parser.next();
assert_eq!(item, None);
format!("{:?}", ParserOptions::new());
}
#[test]
fn test_errors() {
let mut parser = Parser::with_tracker::<OffsetTracker>("|#");
let item = parser.next();
format!("{:?}", item);
assert_eq!(item, Some(Err(Error::DanglingCloseComment {
#[cfg(feature = "miette")]
input: "|#".to_string(),
offset: 0,
span: Span::new(0, 2, ()),
})));
let mut parser = Parser::with_tracker::<OffsetTracker>(")");
let item = parser.next();
format!("{:?}", item);
assert_eq!(item, Some(Err(Error::DanglingCloseParen {
#[cfg(feature = "miette")]
input: "|#".to_string(),
offset: 0,
span: Span::new(0, 1, ()),
})));
let mut parser = Parser::with_tracker::<OffsetTracker>("\"hello\\x\"");
let item = parser.next();
format!("{:?}", item);
assert_eq!(item, Some(Err(Error::UnsupportedEscape {
#[cfg(feature = "miette")]
input: "\"hello\\x\"".to_string(),
offset: 6,
character: 'x',
span: Span::new(0, 8, "hello".to_string()),
})));
let mut parser = Parser::with_tracker::<OffsetTracker>("\"\\n\\x\"");
let item = parser.next();
format!("{:?}", item);
assert_eq!(item, Some(Err(Error::UnsupportedEscape {
#[cfg(feature = "miette")]
input: "\"\\n\\x\"".to_string(),
offset: 3,
character: 'x',
span: Span::new(0, 5, "\n".to_string()),
})));
let mut parser = Parser::with_tracker::<OffsetTracker>("\"hello");
let item = parser.next();
format!("{:?}", item);
assert_eq!(item, Some(Err(Error::MissingEndQuote {
#[cfg(feature = "miette")]
input: "\"hello".to_string(),
offset: 6,
span: Span::new(0, 6, "hello".to_string()),
})));
let mut parser = Parser::with_tracker::<OffsetTracker>("\"hello\\n");
let item = parser.next();
format!("{:?}", item);
assert_eq!(item, Some(Err(Error::MissingEndQuote {
#[cfg(feature = "miette")]
input: "\"hello\\n".to_string(),
offset: 8,
span: Span::new(0, 8, "hello\n".to_string()),
})));
let mut parser = Parser::with_tracker::<OffsetTracker>("(");
let item = parser.next();
format!("{:?}", item);
assert_eq!(item, Some(Err(Error::MissingClosingBracket {
#[cfg(feature = "miette")]
input: "(".to_string(),
start_offset: 0,
})));
}
}