parse default optional expressions in pattern matches

This commit is contained in:
Folkert 2020-07-20 00:52:16 +02:00
parent 3656257191
commit 29c3eebace
6 changed files with 209 additions and 26 deletions

View file

@ -1,5 +1,5 @@
use crate::env::Env; use crate::env::Env;
use crate::expr::Expr; use crate::expr::{canonicalize_expr, Expr};
use crate::num::{finish_parsing_base, finish_parsing_float, finish_parsing_int}; use crate::num::{finish_parsing_base, finish_parsing_float, finish_parsing_int};
use crate::scope::Scope; use crate::scope::Scope;
use roc_module::ident::{Ident, Lowercase, TagName}; use roc_module::ident::{Ident, Lowercase, TagName};
@ -302,6 +302,48 @@ pub fn canonicalize_pattern<'a>(
}, },
}); });
} }
OptionalField(label, loc_default) => {
// an optional DOES introduce the label into scope!
match scope.introduce(
label.into(),
&env.exposed_ident_ids,
&mut env.ident_ids,
region,
) {
Ok(symbol) => {
// TODO use output?
let (can_default, _output) = canonicalize_expr(
env,
var_store,
scope,
loc_default.region,
&loc_default.value,
);
destructs.push(Located {
region: loc_pattern.region,
value: RecordDestruct {
var: var_store.fresh(),
label: Lowercase::from(label),
symbol,
typ: DestructType::Optional(var_store.fresh(), can_default),
},
});
}
Err((original_region, shadow)) => {
env.problem(Problem::RuntimeError(RuntimeError::Shadowing {
original_region,
shadow: shadow.clone(),
}));
// No matter what the other patterns
// are, we're definitely shadowed and will
// get a runtime exception as soon as we
// encounter the first bad pattern.
opt_erroneous = Some(Pattern::Shadowed(original_region, shadow));
}
};
}
_ => unreachable!("Any other pattern should have given a parse error"), _ => unreachable!("Any other pattern should have given a parse error"),
} }
} }
@ -315,7 +357,10 @@ pub fn canonicalize_pattern<'a>(
}) })
} }
RequiredField(_name, _loc_pattern) | OptionalField(_name, _loc_pattern) => { RequiredField(_name, _loc_pattern) => {
unreachable!("should have been handled in RecordDestructure");
}
OptionalField(_name, _loc_pattern) => {
unreachable!("should have been handled in RecordDestructure"); unreachable!("should have been handled in RecordDestructure");
} }

View file

@ -25,9 +25,9 @@ impl<'a> Formattable<'a> for Pattern<'a> {
Pattern::Nested(nested_pat) => nested_pat.is_multiline(), Pattern::Nested(nested_pat) => nested_pat.is_multiline(),
Pattern::RecordDestructure(fields) => fields.iter().any(|f| f.is_multiline()), Pattern::RecordDestructure(fields) => fields.iter().any(|f| f.is_multiline()),
Pattern::RequiredField(_, subpattern) | Pattern::OptionalField(_, subpattern) => { Pattern::RequiredField(_, subpattern) => subpattern.is_multiline(),
subpattern.is_multiline()
} Pattern::OptionalField(_, expr) => expr.is_multiline(),
Pattern::Identifier(_) Pattern::Identifier(_)
| Pattern::GlobalTag(_) | Pattern::GlobalTag(_)

View file

@ -322,7 +322,7 @@ pub enum Pattern<'a> {
/// An optional field pattern, e.g. { x ? Just 0 } -> ... /// An optional field pattern, e.g. { x ? Just 0 } -> ...
/// Can only occur inside of a RecordDestructure /// Can only occur inside of a RecordDestructure
OptionalField(&'a str, &'a Loc<Pattern<'a>>), OptionalField(&'a str, &'a Loc<Expr<'a>>),
/// This is used only to avoid cloning when reordering expressions (e.g. in desugar()). /// This is used only to avoid cloning when reordering expressions (e.g. in desugar()).
/// It lets us take an (&Expr) and create a plain (Expr) from it. /// It lets us take an (&Expr) and create a plain (Expr) from it.
@ -428,8 +428,21 @@ impl<'a> Pattern<'a> {
(RequiredField(x, inner_x), RequiredField(y, inner_y)) => { (RequiredField(x, inner_x), RequiredField(y, inner_y)) => {
x == y && inner_x.value.equivalent(&inner_y.value) x == y && inner_x.value.equivalent(&inner_y.value)
} }
(OptionalField(x, inner_x), OptionalField(y, inner_y)) => { (OptionalField(x, _inner_x), OptionalField(y, _inner_y)) => {
x == y && inner_x.value.equivalent(&inner_y.value) x == y
// TODO
//
// We can give an annotation like so
//
// { x, y } : { x : Int, y : Bool }
// { x, y } = rec
//
// But what about:
//
// { x, y ? False } : { x : Int, y ? Bool }
// { x, y ? False } = rec
//
// inner_x.value.equivalent(&inner_y.value)
} }
(Nested(x), Nested(y)) => x.equivalent(y), (Nested(x), Nested(y)) => x.equivalent(y),

View file

@ -348,10 +348,9 @@ pub fn assigned_expr_field_to_pattern<'a>(
} }
} }
AssignedField::OptionalValue(name, spaces, value) => { AssignedField::OptionalValue(name, spaces, value) => {
let pattern = expr_to_pattern(arena, &value.value)?;
let result = arena.alloc(Located { let result = arena.alloc(Located {
region: value.region, region: value.region,
value: pattern, value: value.value.clone(),
}); });
if spaces.is_empty() { if spaces.is_empty() {
Pattern::OptionalField(name.value, result) Pattern::OptionalField(name.value, result)
@ -378,13 +377,13 @@ pub fn assigned_expr_field_to_pattern<'a>(
/// Used for patterns like { x: Just _ } /// Used for patterns like { x: Just _ }
pub fn assigned_pattern_field_to_pattern<'a>( pub fn assigned_pattern_field_to_pattern<'a>(
arena: &'a Bump, arena: &'a Bump,
assigned_field: &AssignedField<'a, Pattern<'a>>, assigned_field: &AssignedField<'a, Expr<'a>>,
backup_region: Region, backup_region: Region,
) -> Result<Located<Pattern<'a>>, Fail> { ) -> Result<Located<Pattern<'a>>, Fail> {
// the assigned fields always store spaces, but this slice is often empty // the assigned fields always store spaces, but this slice is often empty
Ok(match assigned_field { Ok(match assigned_field {
AssignedField::RequiredValue(name, spaces, value) => { AssignedField::RequiredValue(name, spaces, value) => {
let pattern = value.value.clone(); let pattern = expr_to_pattern(arena, &value.value)?;
let region = Region::span_across(&value.region, &value.region); let region = Region::span_across(&value.region, &value.region);
let result = arena.alloc(Located { let result = arena.alloc(Located {
region: value.region, region: value.region,
@ -929,22 +928,72 @@ fn underscore_pattern<'a>() -> impl Parser<'a, Pattern<'a>> {
fn record_destructure<'a>(min_indent: u16) -> impl Parser<'a, Pattern<'a>> { fn record_destructure<'a>(min_indent: u16) -> impl Parser<'a, Pattern<'a>> {
then( then(
record_without_update!(loc_pattern(min_indent), min_indent), collection!(
move |arena, state, assigned_fields| { char('{'),
let mut patterns = Vec::with_capacity_in(assigned_fields.len(), arena); move |arena: &'a bumpalo::Bump,
for assigned_field in assigned_fields { state: crate::parser::State<'a>|
match assigned_pattern_field_to_pattern( -> crate::parser::ParseResult<'a, Located<crate::ast::Pattern<'a>>> {
arena, use crate::blankspace::{space0, space0_before};
&assigned_field.value, use crate::ident::lowercase_ident;
assigned_field.region, use crate::parser::Either::*;
) { use roc_region::all::Region;
Ok(pattern) => patterns.push(pattern),
Err(e) => return Err((e, state)),
}
}
// You must have a field name, e.g. "email"
let (loc_label, state) = loc!(lowercase_ident()).parse(arena, state)?;
let (spaces, state) = space0(min_indent).parse(arena, state)?;
// Having a value is optional; both `{ email }` and `{ email: blah }` work.
// (This is true in both literals and types.)
let (opt_loc_val, state) = crate::parser::optional(either!(
skip_first!(
char(':'),
space0_before(loc_pattern(min_indent), min_indent)
),
skip_first!(char('?'), space0_before(loc!(expr(min_indent)), min_indent))
))
.parse(arena, state)?;
let answer = match opt_loc_val {
Some(either) => match either {
First(loc_val) => Located {
region: Region::span_across(&loc_label.region, &loc_val.region),
value: Pattern::RequiredField(loc_label.value, arena.alloc(loc_val)),
},
Second(loc_val) => Located {
region: Region::span_across(&loc_label.region, &loc_val.region),
value: Pattern::OptionalField(loc_label.value, arena.alloc(loc_val)),
},
},
// If no value was provided, record it as a Var.
// Canonicalize will know what to do with a Var later.
None => {
if !spaces.is_empty() {
Located {
region: loc_label.region,
value: Pattern::SpaceAfter(
arena.alloc(Pattern::Identifier(loc_label.value)),
spaces,
),
}
} else {
Located {
region: loc_label.region,
value: Pattern::Identifier(loc_label.value),
}
}
}
};
Ok((answer, state))
},
char(','),
char('}'),
min_indent
),
move |_arena, state, loc_patterns| {
Ok(( Ok((
Pattern::RecordDestructure(patterns.into_bump_slice()), Pattern::RecordDestructure(loc_patterns.into_bump_slice()),
state, state,
)) ))
}, },

View file

@ -2629,4 +2629,16 @@ mod solve_expr {
"{ a : { x : Num a, y : Float, z : c }, b : { blah : Str, x : Num a, y : Float, z : c } }", "{ a : { x : Num a, y : Float, z : c }, b : { blah : Str, x : Num a, y : Float, z : c } }",
); );
} }
#[test]
fn optional_field_function() {
infer_eq_without_problem(
indoc!(
r#"
\{ x, y ? 0 } -> x + y
"#
),
"{ x : Num a, y ? Num a }* -> Num a",
);
}
} }

View file

@ -3011,4 +3011,68 @@ mod solve_uniq_expr {
"Attr * (Attr Shared (Num (Attr Shared *)) -> Attr * (Num (Attr * *)))", "Attr * (Attr Shared (Num (Attr Shared *)) -> Attr * (Num (Attr * *)))",
); );
} }
// OPTIONAL RECORD FIELDS
#[test]
fn optional_field_unifies_with_missing() {
infer_eq(
indoc!(
r#"
negatePoint : { x : Int, y : Int, z ? Num c } -> { x : Int, y : Int, z : Num c }
negatePoint { x: 1, y: 2 }
"#
),
"Attr * { x : (Attr * Int), y : (Attr * Int), z : (Attr * (Num (Attr * c))) }",
);
}
#[test]
fn open_optional_field_unifies_with_missing() {
infer_eq(
indoc!(
r#"
negatePoint : { x : Int, y : Int, z ? Num c }r -> { x : Int, y : Int, z : Num c }r
a = negatePoint { x: 1, y: 2 }
b = negatePoint { x: 1, y: 2, blah : "hi" }
{ a, b }
"#
),
"Attr * { a : (Attr * { x : (Attr * Int), y : (Attr * Int), z : (Attr * (Num (Attr * c))) }), b : (Attr * { blah : (Attr * Str), x : (Attr * Int), y : (Attr * Int), z : (Attr * (Num (Attr * c))) }) }"
);
}
#[test]
fn optional_field_unifies_with_present() {
infer_eq(
indoc!(
r#"
negatePoint : { x : Num a, y : Num b, z ? c } -> { x : Num a, y : Num b, z : c }
negatePoint { x: 1, y: 2.1, z: 0x3 }
"#
),
"Attr * { x : (Attr * (Num (Attr * a))), y : (Attr * Float), z : (Attr * Int) }",
);
}
#[test]
fn open_optional_field_unifies_with_present() {
infer_eq(
indoc!(
r#"
negatePoint : { x : Num a, y : Num b, z ? c }r -> { x : Num a, y : Num b, z : c }r
a = negatePoint { x: 1, y: 2.1 }
b = negatePoint { x: 1, y: 2.1, blah : "hi" }
{ a, b }
"#
),
"Attr * { a : (Attr * { x : (Attr * (Num (Attr * a))), y : (Attr * Float), z : (Attr * c) }), b : (Attr * { blah : (Attr * Str), x : (Attr * (Num (Attr * a))), y : (Attr * Float), z : (Attr * c) }) }"
);
}
} }