Add if-expressions

This commit is contained in:
Richard Feldman 2019-05-07 21:30:02 -04:00
parent a842ce376d
commit 8222c11a9e
3 changed files with 100 additions and 17 deletions

View file

@ -14,6 +14,7 @@ pub enum Expr {
Apply(Box<Expr>, Box<Expr>), Apply(Box<Expr>, Box<Expr>),
Operator(Box<Expr>, Operator, Box<Expr>), Operator(Box<Expr>, Operator, Box<Expr>),
If(Box<Expr>, Box<Expr>, Box<Expr>),
SyntaxProblem(String), SyntaxProblem(String),
} }

View file

@ -3,10 +3,10 @@ use expr::Expr;
use std::char; use std::char;
use combine::parser::char::{char, space, spaces, digit, hex_digit, HexDigit, alpha_num}; use combine::parser::char::{char, string, space, spaces, digit, hex_digit, HexDigit, alpha_num};
use combine::parser::repeat::{many, count_min_max}; use combine::parser::repeat::{many, count_min_max};
use combine::parser::item::{any, satisfy_map, value}; use combine::parser::item::{any, satisfy_map, value};
use combine::parser::combinator::{look_ahead}; use combine::parser::combinator::{look_ahead, not_followed_by};
use combine::{attempt, choice, eof, many1, parser, Parser, optional, between, unexpected_any}; use combine::{attempt, choice, eof, many1, parser, Parser, optional, between, unexpected_any};
use combine::error::{Consumed, ParseError}; use combine::error::{Consumed, ParseError};
use combine::stream::{Stream}; use combine::stream::{Stream};
@ -25,12 +25,12 @@ fn whitespace_or_eof<I>() -> impl Parser<Input = I, Output = ()>
where I: Stream<Item = char>, where I: Stream<Item = char>,
I::Error: ParseError<I::Item, I::Range, I::Position> { I::Error: ParseError<I::Item, I::Range, I::Position> {
choice(( choice((
whitespace(), spaces1(),
eof().with(value(())) eof().with(value(()))
)) ))
} }
fn whitespace<I>() -> impl Parser<Input = I, Output = ()> fn spaces1<I>() -> impl Parser<Input = I, Output = ()>
where I: Stream<Item = char>, where I: Stream<Item = char>,
I::Error: ParseError<I::Item, I::Range, I::Position> { I::Error: ParseError<I::Item, I::Range, I::Position> {
// TODO we discard this Vec, so revise this to not allocate one // TODO we discard this Vec, so revise this to not allocate one
@ -48,28 +48,53 @@ parser! {
string_literal(), string_literal(),
number_literal(), number_literal(),
char_literal(), char_literal(),
if_expr(),
func_or_var(), func_or_var(),
)).skip(spaces()).and( ))
.and(
// Optionally follow the expression with an operator, // Optionally follow the expression with an operator,
// //
// e.g. In the expression (1 + 2), the subexpression 1 // e.g. In the expression (1 + 2), the subexpression 1
// is followed by the operator + and another subexpression, 2 // is followed by the operator + and another subexpression, 2
optional( optional(
operator() attempt(
.skip(spaces()) spaces()
.and(expr_body()) .with(operator())
.skip(spaces())
.and(expr_body())
.skip(spaces())
)
) )
).skip(spaces()).map(|(v1, opt_op)| { ).map(|(expr1, opt_op)| {
match opt_op { match opt_op {
None => v1, None => expr1,
Some((op, v2)) => { Some((op, expr2)) => {
Expr::Operator(Box::new(v1), op, Box::new(v2)) Expr::Operator(Box::new(expr1), op, Box::new(expr2))
}, },
} }
}) })
} }
} }
pub fn if_expr<I>() -> impl Parser<Input = I, Output = Expr>
where I: Stream<Item = char>,
I::Error: ParseError<I::Item, I::Range, I::Position>
{
string("if").with(spaces1())
.with(expr_body()).skip(spaces1())
.skip(string("then")).skip(spaces1())
.and(expr_body()).skip(spaces1())
.skip(string("else")).skip(spaces1())
.and(expr_body())
.map(|((conditional, then_branch), else_branch)|
Expr::If(
Box::new(conditional),
Box::new(then_branch),
Box::new(else_branch)
)
)
}
pub fn parenthetical_expr<I>() -> impl Parser<Input = I, Output = Expr> pub fn parenthetical_expr<I>() -> impl Parser<Input = I, Output = Expr>
where I: Stream<Item = char>, where I: Stream<Item = char>,
I::Error: ParseError<I::Item, I::Range, I::Position> I::Error: ParseError<I::Item, I::Range, I::Position>
@ -80,7 +105,12 @@ where I: Stream<Item = char>,
// Parenthetical expressions can optionally be followed by // Parenthetical expressions can optionally be followed by
// whitespace and an expr, meaning this is function application! // whitespace and an expr, meaning this is function application!
optional( optional(
attempt(whitespace().with(expr_body())) attempt(
spaces1()
// Keywords like "then" and "else" are not function application!
.skip(not_followed_by(choice((string("then"), string("else")))))
.with(expr_body())
)
) )
).map(|(expr1, opt_expr2)| ).map(|(expr1, opt_expr2)|
match opt_expr2 { match opt_expr2 {
@ -110,7 +140,11 @@ where I: Stream<Item = char>,
{ {
ident() ident()
.and(optional( .and(optional(
attempt(whitespace().with(expr_body())) attempt(
spaces1()
.skip(not_followed_by(choice((string("then"), string("else")))))
.with(expr_body())
)
)).map(|pair| )).map(|pair|
match pair { match pair {
( Ok(str), Some(arg) ) => Expr::Func(str, Box::new(arg)), ( Ok(str), Some(arg) ) => Expr::Func(str, Box::new(arg)),
@ -285,11 +319,25 @@ pub fn number_literal<I>() -> impl Parser<Input = I, Output = Expr>
where I: Stream<Item = char>, where I: Stream<Item = char>,
I::Error: ParseError<I::Item, I::Range, I::Position> I::Error: ParseError<I::Item, I::Range, I::Position>
{ {
// Digits before the decimal point can be space-separated // We expect these to be digits, but read any alphanumeric characters
// e.g. one million can be written as 1 000 000 // because it could turn out they're malformed identifiers which
let digits_before_decimal = many1::<Vec<_>, _>(alpha_num().skip(optional(char(' ')))); // happen to begin with a number. We'll check for that at the end.
let digits_after_decimal = many1::<Vec<_>, _>(alpha_num()); let digits_after_decimal = many1::<Vec<_>, _>(alpha_num());
// Digits before the decimal point can be space-separated
// e.g. one million can be written as 1 000 000
let digits_before_decimal = many1::<Vec<_>, _>(
alpha_num().skip(optional(
attempt(
char(' ').skip(
// Don't mistake keywords like `then` and `else` for
// space-separated digits!
not_followed_by(choice((string("then"), string("else"))))
)
)
))
);
optional(char('-')) optional(char('-'))
// Do this lookahead to decide if we should parse this as a number. // Do this lookahead to decide if we should parse this as a number.
// This matters because once we commit to parsing it as a number, // This matters because once we commit to parsing it as a number,

View file

@ -415,6 +415,40 @@ mod tests {
expect_parsed_func_error("(f 1"); expect_parsed_func_error("(f 1");
} }
// IF
#[test]
fn parse_if_space_separated_number() {
assert_eq!(
parse::expr().parse("if 12 34 5 then 5 4 32 1 else 1 3 37"),
Ok(
(
If(
Box::new(Int(12345)),
Box::new(Int(54321)),
Box::new(Int(1337))
),
"")
)
);
}
#[test]
fn parse_if() {
assert_eq!(
parse::expr().parse("if foo then 1 else 2"),
Ok(
(
If(
Box::new(Var("foo".to_string())),
Box::new(Int(1)),
Box::new(Int(2))
),
"")
)
);
}
// COMPLEX EXPRESSIONS // COMPLEX EXPRESSIONS
#[test] #[test]