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
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,
|
|
})));
|
|
}
|
|
} |