Skip to content

Instantly share code, notes, and snippets.

@emadb
Last active May 13, 2025 14:03
Show Gist options
  • Save emadb/aae4e95bcd34c68787389caad7cedc15 to your computer and use it in GitHub Desktop.
Save emadb/aae4e95bcd34c68787389caad7cedc15 to your computer and use it in GitHub Desktop.
Parser Combinators in Rust
#[derive(Debug, Clone, PartialEq)]
struct ParseError(String);
#[derive(Debug, PartialEq)]
struct ParseOutput<T> {
result: T,
rest: String,
}
type ParseResult<T> = Result<ParseOutput<T>, ParseError>;
pub struct Parser<T> {
parse_fn: Box<dyn Fn(&str) -> ParseResult<T>>,
}
impl<T> Parser<T> {
fn new<F>(parse_fn: F) -> Self
where
F: Fn(&str) -> ParseResult<T> + 'static,
{
Parser {
parse_fn: Box::new(parse_fn),
}
}
fn run(&self, input: &str) -> ParseResult<T> {
(self.parse_fn)(input)
}
}
fn parse_char(char_to_match: char) -> Parser<char> {
let parse_fn = move |input: &str| {
let chars: Vec<char> = input.chars().collect();
let head = chars.first().unwrap();
let tail: &[char] = &chars[1..];
if *head == char_to_match {
Ok(ParseOutput::<char> {
result: *head,
rest: tail.iter().collect::<String>(),
})
} else {
Err(ParseError("TODO: Error".to_string()))
}
};
Parser::new(parse_fn)
}
fn bind<T1, T2>(p1: Parser<T1>, p2: Parser<T2>) -> impl Fn(String) -> ParseResult<(T1, T2)> {
move |input: String| -> ParseResult<(T1, T2)> {
match (p1.parse_fn)(&input) {
Ok(o1) => match (p2.parse_fn)(&o1.rest) {
Ok(o2) => Ok(ParseOutput::<(T1, T2)> {
result: (o1.result, o2.result),
rest: o2.rest,
}),
Err(e2) => Err(e2),
},
Err(e1) => Err(e1),
}
}
}
fn between<T1, T2, T3>(
p1: Parser<T1>,
p2: Parser<T2>,
p3: Parser<T3>,
) -> impl Fn(String) -> ParseResult<T2> {
move |input: String| -> ParseResult<T2> {
match (p1.parse_fn)(&input) {
Ok(o1) => match (p2.parse_fn)(&o1.rest) {
Ok(o2) => match (p3.parse_fn)(&o2.rest) {
Ok(o3) => Ok(ParseOutput::<T2> {
result: o2.result,
rest: o3.rest,
}),
Err(e3) => Err(e3),
},
Err(a2) => Err(a2),
},
Err(e1) => Err(e1),
}
}
}
fn or_else<T1>(p1: Parser<T1>, p2: Parser<T1>) -> impl Fn(String) -> ParseResult<T1> {
move |input: String| -> ParseResult<T1> {
match (p1.parse_fn)(&input) {
Ok(o1) => Ok(ParseOutput::<T1> {
result: o1.result,
rest: o1.rest,
}),
Err(_) => (p2.parse_fn)(&input),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
// <134>May 6 17:52:12 127.0.0.1 example of a nice log message\n
#[test]
fn parse_string() {
let input_string = "abcd";
let char_parser = parse_char('a');
let result = char_parser.run(input_string);
let expected_parse_output = ParseOutput {
result: 'a',
rest: String::from("bcd"),
};
assert_eq!(result, Ok(expected_parse_output));
}
#[test]
fn bind_test() {
let input_string = String::from("abcd");
let pa: Parser<char> = parse_char('a');
let pb: Parser<char> = parse_char('b');
let result = bind(pa, pb)(input_string);
let expected_parse_output = ParseOutput {
result: ('a', 'b'),
rest: String::from("cd"),
};
assert_eq!(result, Ok(expected_parse_output));
}
#[test]
fn between_test() {
let input_string = String::from("<a>");
let pa: Parser<char> = parse_char('<');
let pb: Parser<char> = parse_char('a');
let pc: Parser<char> = parse_char('>');
let result = between(pa, pb, pc)(input_string);
let expected_parse_output = ParseOutput {
result: 'a',
rest: String::from(""),
};
assert_eq!(result, Ok(expected_parse_output));
}
#[test]
fn or_else_test() {
let input_string = String::from("abc");
let pa: Parser<char> = parse_char('z');
let pb: Parser<char> = parse_char('a');
let result = or_else(pa, pb)(input_string);
let expected_parse_output = ParseOutput {
result: 'a',
rest: String::from("bc"),
};
assert_eq!(result, Ok(expected_parse_output));
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment