From 6afeedf10eadab4af583d22d1943ca61a6b130a0 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Wed, 12 Jun 2019 22:49:36 -0400 Subject: [PATCH] Add support for pattern matching on numbers --- src/eval.rs | 37 ++++++++++++++++++++++++ src/expr.rs | 3 ++ src/parse.rs | 79 +++++++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 118 insertions(+), 1 deletion(-) diff --git a/src/eval.rs b/src/eval.rs index fb8d1d4447..c00d25b18b 100644 --- a/src/eval.rs +++ b/src/eval.rs @@ -85,6 +85,14 @@ pub fn scoped_eval(expr: Expr, vars: &Scope) -> Evaluated { } }, + Let(Integer(_), _, _) => { + panic!("You cannot assign integers to other values!"); + }, + + Let(Fraction(_, _), _, _) => { + panic!("You cannot assign fractions to other values!"); + }, + Let(Variant(_name, _patterns), _definition, _in_expr) => { panic!("Pattern matching on variants is not yet supported!"); }, @@ -329,6 +337,35 @@ fn pattern_match(evaluated: Evaluated, pattern: Pattern, vars: &mut Scope) -> Re )) } }, + + Integer(pattern_num) => { + match evaluated { + Evaluated(Expr::Int(evaluated_num)) => { + if pattern_num == evaluated_num { + Ok(()) + } else { + Err(Problem::NotEqual) + } + }, + + Evaluated(expr) => Err(TypeMismatch( + format!("Wanted a `{}`, but was given `{}`.", "{}", expr) + )) + } + }, + + Fraction(_pattern_numerator, _pattern_denominator) => { + match evaluated { + Evaluated(Expr::Frac(_evaluated_numerator, _evaluated_denominator)) => { + panic!("Can't handle pattern matching on fracs yet."); + }, + + Evaluated(expr) => Err(TypeMismatch( + format!("Wanted a `{}`, but was given `{}`.", "{}", expr) + )) + } + } + Variant(pattern_variant_name, opt_pattern_contents) => { match evaluated { Evaluated(ApplyVariant(applied_variant_name, opt_applied_contents)) => { diff --git a/src/expr.rs b/src/expr.rs index 9f6a29b2d8..9042208943 100644 --- a/src/expr.rs +++ b/src/expr.rs @@ -101,6 +101,7 @@ pub enum Problem { TypeMismatch(String), ReassignedVarName(String), WrongArity(u32 /* Expected */, u32 /* Provided */), + NotEqual, // Used when (for example) a string literal pattern match fails NoBranchesMatched, } @@ -108,6 +109,8 @@ pub enum Problem { pub enum Pattern { Identifier(String), Variant(String, Option>), + Integer(i64), + Fraction(i64, u64), EmptyRecord, Underscore } diff --git a/src/parse.rs b/src/parse.rs index 06e0c37cc6..2291abdca1 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -411,7 +411,8 @@ parser! { char('_').map(|_| Pattern::Underscore), string("{}").map(|_| Pattern::EmptyRecord), ident().map(|name| Pattern::Identifier(name)), - match_variant(min_indent) + match_variant(min_indent), + number_pattern(), )) } } @@ -727,3 +728,79 @@ where I: Stream, }) } +/// TODO find a way to remove the code duplication between this and number_literal +/// without sacrificing performance. I attempted to do this in 0062e83d03d389f0f07e33e1e7929e77825d774f +/// but couldn't figure out how to address the resulting compiler error, which was: +/// "cannot move out of captured outer variable in an `FnMut` closure" +pub fn number_pattern() -> impl Parser +where I: Stream, + I::Error: ParseError +{ + // We expect these to be digits, but read any alphanumeric characters + // because it could turn out they're malformed identifiers which + // happen to begin with a number. We'll check for that at the end. + let digits_after_decimal = many1::, _>(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::, _>( + 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"), string("when")))) + ) + ) + )) + ); + + optional(char('-')) + // 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, + // we may discover non-digit chars, indicating this is actually an + // invalid identifier. (e.g. "523foo" looks like a number, but turns + // out to be an invalid identifier on closer inspection.) + .and(look_ahead(digit())) + .and(digits_before_decimal) + .and(optional(char('.').with(digits_after_decimal))) + .then(|(((opt_minus, _), int_digits), decimals): (((Option, _), Vec), Option>)| { + let is_positive = opt_minus.is_none(); + + // TODO check length of digits and make sure not to overflow + let int_str: String = int_digits.into_iter().collect(); + + match ( int_str.parse::(), decimals ) { + (Ok(int_val), None) => { + if is_positive { + value(Pattern::Integer(int_val as i64)).right() + } else { + value(Pattern::Integer(-int_val as i64)).right() + } + }, + (Ok(int_val), Some(nums)) => { + let decimal_str: String = nums.into_iter().collect(); + // calculate numerator and denominator + // e.g. 123.45 == 12345 / 100 + let denom = (10 as i64).pow(decimal_str.len() as u32); + + match decimal_str.parse::() { + Ok(decimal) => { + let numerator = (int_val * denom) + (decimal as i64); + + if is_positive { + value(Pattern::Fraction(numerator, denom as u64)).right() + } else { + value(Pattern::Fraction(-numerator, denom as u64)).right() + } + }, + Err(_) => { + unexpected_any("non-digit characters after decimal point in a number literal").left() + } + } + }, + (Err(_), _) => + unexpected_any("looked like a number but was actually malformed identifier").left() + } + }) +}