diff --git a/compiler/can/src/pattern.rs b/compiler/can/src/pattern.rs index a8bc6ba307..f1978acefd 100644 --- a/compiler/can/src/pattern.rs +++ b/compiler/can/src/pattern.rs @@ -1,5 +1,5 @@ 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::scope::Scope; 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"), } } @@ -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"); } diff --git a/compiler/fmt/src/pattern.rs b/compiler/fmt/src/pattern.rs index b43f27ac27..361a4ac9df 100644 --- a/compiler/fmt/src/pattern.rs +++ b/compiler/fmt/src/pattern.rs @@ -25,9 +25,9 @@ impl<'a> Formattable<'a> for Pattern<'a> { Pattern::Nested(nested_pat) => nested_pat.is_multiline(), Pattern::RecordDestructure(fields) => fields.iter().any(|f| f.is_multiline()), - Pattern::RequiredField(_, subpattern) | Pattern::OptionalField(_, subpattern) => { - subpattern.is_multiline() - } + Pattern::RequiredField(_, subpattern) => subpattern.is_multiline(), + + Pattern::OptionalField(_, expr) => expr.is_multiline(), Pattern::Identifier(_) | Pattern::GlobalTag(_) diff --git a/compiler/parse/src/ast.rs b/compiler/parse/src/ast.rs index 1fe3242a85..0e7ba96f4b 100644 --- a/compiler/parse/src/ast.rs +++ b/compiler/parse/src/ast.rs @@ -322,7 +322,7 @@ pub enum Pattern<'a> { /// An optional field pattern, e.g. { x ? Just 0 } -> ... /// Can only occur inside of a RecordDestructure - OptionalField(&'a str, &'a Loc>), + OptionalField(&'a str, &'a Loc>), /// 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. @@ -428,8 +428,21 @@ impl<'a> Pattern<'a> { (RequiredField(x, inner_x), RequiredField(y, inner_y)) => { x == y && inner_x.value.equivalent(&inner_y.value) } - (OptionalField(x, inner_x), OptionalField(y, inner_y)) => { - x == y && inner_x.value.equivalent(&inner_y.value) + (OptionalField(x, _inner_x), OptionalField(y, _inner_y)) => { + 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), diff --git a/compiler/parse/src/expr.rs b/compiler/parse/src/expr.rs index 5e7930c698..29022dfbe1 100644 --- a/compiler/parse/src/expr.rs +++ b/compiler/parse/src/expr.rs @@ -348,10 +348,9 @@ pub fn assigned_expr_field_to_pattern<'a>( } } AssignedField::OptionalValue(name, spaces, value) => { - let pattern = expr_to_pattern(arena, &value.value)?; let result = arena.alloc(Located { region: value.region, - value: pattern, + value: value.value.clone(), }); if spaces.is_empty() { Pattern::OptionalField(name.value, result) @@ -378,13 +377,13 @@ pub fn assigned_expr_field_to_pattern<'a>( /// Used for patterns like { x: Just _ } pub fn assigned_pattern_field_to_pattern<'a>( arena: &'a Bump, - assigned_field: &AssignedField<'a, Pattern<'a>>, + assigned_field: &AssignedField<'a, Expr<'a>>, backup_region: Region, ) -> Result>, Fail> { // the assigned fields always store spaces, but this slice is often empty Ok(match assigned_field { 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 result = arena.alloc(Located { 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>> { then( - record_without_update!(loc_pattern(min_indent), min_indent), - move |arena, state, assigned_fields| { - let mut patterns = Vec::with_capacity_in(assigned_fields.len(), arena); - for assigned_field in assigned_fields { - match assigned_pattern_field_to_pattern( - arena, - &assigned_field.value, - assigned_field.region, - ) { - Ok(pattern) => patterns.push(pattern), - Err(e) => return Err((e, state)), - } - } + collection!( + char('{'), + move |arena: &'a bumpalo::Bump, + state: crate::parser::State<'a>| + -> crate::parser::ParseResult<'a, Located>> { + use crate::blankspace::{space0, space0_before}; + use crate::ident::lowercase_ident; + use crate::parser::Either::*; + use roc_region::all::Region; + // 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(( - Pattern::RecordDestructure(patterns.into_bump_slice()), + Pattern::RecordDestructure(loc_patterns.into_bump_slice()), state, )) }, diff --git a/compiler/solve/tests/solve_expr.rs b/compiler/solve/tests/solve_expr.rs index 6a43144dbd..fe3d9be14f 100644 --- a/compiler/solve/tests/solve_expr.rs +++ b/compiler/solve/tests/solve_expr.rs @@ -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 } }", ); } + + #[test] + fn optional_field_function() { + infer_eq_without_problem( + indoc!( + r#" + \{ x, y ? 0 } -> x + y + "# + ), + "{ x : Num a, y ? Num a }* -> Num a", + ); + } } diff --git a/compiler/solve/tests/solve_uniq_expr.rs b/compiler/solve/tests/solve_uniq_expr.rs index 14289dd6f5..3d47a59a3c 100644 --- a/compiler/solve/tests/solve_uniq_expr.rs +++ b/compiler/solve/tests/solve_uniq_expr.rs @@ -3011,4 +3011,68 @@ mod solve_uniq_expr { "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) }) }" + ); + } }