Add support for pattern matching on numbers

This commit is contained in:
Richard Feldman 2019-06-12 22:49:36 -04:00
parent 0062e83d03
commit 6afeedf10e
3 changed files with 118 additions and 1 deletions

View File

@ -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)) => {

View File

@ -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<Vec<Pattern>>),
Integer(i64),
Fraction(i64, u64),
EmptyRecord,
Underscore
}

View File

@ -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<Item = char, Position = IndentablePosition>,
})
}
/// 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<I>() -> impl Parser<Input = I, Output = Pattern>
where I: Stream<Item = char, Position = IndentablePosition>,
I::Error: ParseError<I::Item, I::Range, I::Position>
{
// 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::<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"), 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<char>, _), Vec<char>), Option<Vec<char>>)| {
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::<i64>(), 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::<u32>() {
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()
}
})
}