diff --git a/src/expr.rs b/src/expr.rs index 26acbb3f4f..7c89ef5b1d 100644 --- a/src/expr.rs +++ b/src/expr.rs @@ -20,5 +20,5 @@ pub enum Expr { #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] pub enum Operator { - Plus, Minus, Star, Slash, DoubleSlash, + Plus, Minus, Star, Slash, DoubleSlash, Equals, } diff --git a/src/parse.rs b/src/parse.rs index 5aeb60be90..632e065644 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -5,10 +5,10 @@ use std::char; use parse_state::{IndentablePosition}; use combine::parser::char::{char, string, spaces, digit, hex_digit, HexDigit, alpha_num}; -use combine::parser::repeat::{many, count_min_max, skip_until}; +use combine::parser::repeat::{many, count_min_max}; use combine::parser::item::{any, satisfy_map, value, position}; use combine::parser::combinator::{look_ahead, not_followed_by}; -use combine::{attempt, choice, eof, many1, parser, Parser, optional, between, unexpected, unexpected_any}; +use combine::{attempt, choice, eof, many1, parser, Parser, optional, between, unexpected_any}; use combine::error::{Consumed, ParseError}; use combine::stream::{Stream, Positioned}; @@ -45,6 +45,12 @@ where I: Stream, many::, _>(choice((char(' '), char('\n')))).with(value(())) } +fn whitespace1() -> impl Parser +where I: Stream, + I::Error: ParseError { + many1::, _>(choice((char(' '), char('\n')))).with(value(())) +} + fn spaces1() -> impl Parser where I: Stream, @@ -71,8 +77,8 @@ where I: Stream, fn indented_space(min_indent: i32) -> impl Parser where I: Stream, I::Error: ParseError { - indentation().then(move |indent| { - if indent >= min_indent { + position().then(move |pos: IndentablePosition| { + if pos.is_indenting || pos.indent_col >= min_indent { choice((char(' '), char('\n'))).left() } else { unexpected_any("bad indentation on let-expression").right() @@ -95,8 +101,8 @@ parser! { number_literal(), char_literal(), if_expr(min_indent), - func_or_var(min_indent), let_expr(min_indent), + func_or_var(min_indent), )) .and( // Optionally follow the expression with an operator, @@ -174,6 +180,7 @@ where I: Stream, I::Error: ParseError { choice(( + string("==").map(|_| Operator::Equals), char('+').map(|_| Operator::Plus), char('-').map(|_| Operator::Minus), char('*').map(|_| Operator::Star), @@ -185,30 +192,32 @@ pub fn let_expr(min_indent: i32) -> impl Parser where I: Stream, I::Error: ParseError { - ident().and(indentation()) + attempt( + ident().and(indentation()).message("malformed identifier inside declaration") + .skip(whitespace()).message("whitespace after identifier") + .and( + char('=').with(indentation()) + // If the "=" after the identifier turns out to be + // either "==" or "=>" then this is not a declaration! + .skip(not_followed_by(choice((char('='), char('>'))))) + ) + ) .skip(whitespace()) - .and(char('=').with(indentation())) - .skip(whitespace()) - .then(|((var_name, original_indent), equals_sign_indent)| { - panic!("original_indent {}, equals_sign_indent {}", original_indent, equals_sign_indent); - if equals_sign_indent < original_indent /* `<` because '=' should be same indent or greater */ { + .then(move |((var_name, original_indent), equals_sign_indent)| { + if original_indent < min_indent { + unexpected_any("this declaration is outdented too far").left() + } else if equals_sign_indent < original_indent /* `<` because '=' should be same indent or greater */ { unexpected_any("the = in this declaration seems outdented").left() } else { expr_body(original_indent + 1 /* declaration body must be indented relative to original decl */) - // .skip( - // skip_until(indentation().then(move |final_expr_indent| { - // // The final expr must be back at *exactly* the original indentation. - // if final_expr_indent == original_indent { - // value(()).left() - // } else { - // // This just means we haven't found the final expr yet. - // unexpected("this declaration is missing its return expression").right() - // } - // })) - // ) - .and(expr_body(original_indent)) - .map(move |(var_expr, in_expr)| { - Expr::Let(var_name.to_owned(), Box::new(var_expr), Box::new(in_expr)) + .skip(whitespace1()) + .and(expr_body(original_indent).and(indentation())) + .then(move |(var_expr, (in_expr, in_expr_indent))| { + if in_expr_indent != original_indent { + unexpected_any("the return expression was indented differently from the original declaration").left() + } else { + value(Expr::Let(var_name.to_owned(), Box::new(var_expr), Box::new(in_expr))).right() + } }).right() } }) @@ -219,7 +228,6 @@ where I: Stream, I::Error: ParseError { ident() - .skip(not_followed_by(attempt(spaces().with(char('='))))) .and(optional( attempt( indented_spaces1(min_indent) @@ -247,13 +255,13 @@ where I: Stream, let ident_str:String = chars.into_iter().collect(); if valid_start_char { - if ident_str == "if" { - unexpected_any("Reserved keyword `if`").left() - } else { - value(ident_str).right() + match ident_str.as_str() { + "if" => unexpected_any("Reserved keyword `if`").left(), + "then" => unexpected_any("Reserved keyword `then`").left(), + _ => value(ident_str).right() } } else { - unexpected_any("Starting character").left() + unexpected_any("First character in an identifier that was not a lowercase letter").left() } }) } @@ -453,12 +461,12 @@ where I: Stream, } }, Err(_) => { - unexpected_any("TODO Non-digit chars after decimal point in number literal").left() + unexpected_any("non-digit characters after decimal point in a number literal").left() } } }, (Err(_), _) => - unexpected_any("TODO looked like a number but was actually malformed ident").left() + unexpected_any("looked like a number but was actually malformed ident").left() } }) } diff --git a/src/parse_state.rs b/src/parse_state.rs index afa72d6590..f16f2f099b 100644 --- a/src/parse_state.rs +++ b/src/parse_state.rs @@ -16,7 +16,7 @@ use combine::stream::state::{Positioner, RangePositioner}; #[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)] pub struct IndentablePosition { /// Current line of the input - pub row: i32, + pub line: i32, /// Current column of the input pub column: i32, @@ -32,14 +32,14 @@ clone_resetable! { () IndentablePosition } impl Default for IndentablePosition { fn default() -> Self { - IndentablePosition { row: 1, column: 1, indent_col: 1, is_indenting: true } + IndentablePosition { line: 1, column: 1, indent_col: 1, is_indenting: true } } } impl fmt::Display for IndentablePosition { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "row: {}, column: {}, indent_col: {}, is_indenting: {}", - self.row, self.column, self.indent_col, self.is_indenting) + write!(f, "line: {}, column: {}, indent_col: {}, is_indenting: {}", + self.line, self.column, self.indent_col, self.is_indenting) } } @@ -62,7 +62,7 @@ impl Positioner for IndentablePosition { match *item { '\n' => { self.column = 1; - self.row += 1; + self.line += 1; self.indent_col = 1; self.is_indenting = true; }, diff --git a/tests/test_parse.rs b/tests/test_parse.rs index 4bc18a21f1..049ab4c87e 100644 --- a/tests/test_parse.rs +++ b/tests/test_parse.rs @@ -11,7 +11,7 @@ mod tests { use roc::parse; use roc::parse_state::{IndentablePosition}; use combine::{Parser, eof}; - use combine::error::{ParseError, StringStreamError}; + use combine::error::{ParseError}; use combine::stream::{Stream}; use combine::easy; use combine::stream::state::{State}; @@ -233,15 +233,18 @@ mod tests { #[test] fn parse_single_operator_with_var() { assert_eq!( - parse_standalone("x + 1"), + // It's important that this isn't mistaken for + // a declaration like (x = 1) + parse_standalone("x == 1"), Ok((Operator( Box::new(Var("x".to_string())), - Plus, + Equals, Box::new(Int(1)) ), "")) ); } + #[test] fn parse_single_operator() { match parse_standalone("1234 + 567") { @@ -250,6 +253,7 @@ mod tests { assert_eq!(op, Plus); assert_eq!(*v2, Int(567)); }, + _ => panic!("Expression didn't parse"), } } @@ -575,7 +579,73 @@ mod tests { // LET #[test] - fn parse_let() { + fn parse_let_returning_number() { + assert_eq!( + // let x = 5 in -10 + parse_standalone("x = 5\n-10"), + Ok(( + Let("x".to_string(), Box::new(Int(5)), Box::new(Int(-10))), + "") + ) + ); + + assert_eq!( + // let x = 5 in 10 + parse_standalone("x=5\n-10"), + Ok(( + Let("x".to_string(), Box::new(Int(5)), Box::new(Int(-10))), + "") + ) + ); + } + + #[test] + fn parse_let_with_operator() { + assert_eq!( + // let x = 5 + 10 in -20 + parse_standalone("x = 5 + 10\n-20"), + Ok(( + Let("x".to_string(), Box::new(Int(5)), Box::new(Int(-10))), + "") + ) + ); + + assert_eq!( + // let x = 5 + 10 in -20 + parse_standalone("x=5\n +10\n-20"), + Ok(( + Let("x".to_string(), Box::new(Int(5)), Box::new(Int(-10))), + "") + ) + ); + } + + #[test] + fn parse_invalid_let_returning_number() { + assert!( + parse_standalone("x=5\n -10").is_err(), + "Expected parsing error" + ); + } + + #[test] + fn parse_nested_let() { + assert_eq!( + // let x = 5 in let y = 12 in 3 + parse_standalone("x = 5\ny = 12\n3"), + Ok(( + Let("x".to_string(), Box::new(Int(5)), + Box::new( + Let("y".to_string(), Box::new(Int(12)), + Box::new(Int(3)) + ))), + "") + ) + ); + } + + #[test] + fn parse_let_returning_var() { assert_eq!( parse_standalone("x=5\nx"), Ok(( @@ -587,12 +657,9 @@ mod tests { #[test] fn parse_bad_equals_indent_let() { - assert_eq!( - parse_standalone(" x=\n5\n\n5"), - Ok(( - Let("x".to_string(), Box::new(Int(5)), Box::new(Var("x".to_string()))), - "") - ) + assert!( + parse_standalone(" x=\n5\n\n5").is_err(), + "Expected parsing error" ); } }