diff --git a/crates/ditto-ast/src/expression.rs b/crates/ditto-ast/src/expression.rs index 15ac7c80e..32a8c0b25 100644 --- a/crates/ditto-ast/src/expression.rs +++ b/crates/ditto-ast/src/expression.rs @@ -58,6 +58,25 @@ pub enum Expression { /// The expression to evaluate otherwise. false_clause: Box, }, + /// A pattern match. + /// + /// ```ditto + /// match some_expr with + /// | Pattern -> another_expr + /// ``` + Match { + /// The source span for this expression. + span: Span, + + /// The type of the expressions in the `arms`. + match_type: Type, + + /// Expression to be matched. + expression: Box, + + /// Patterns to be matched against and their corresponding expressions. + arms: NonEmpty<(Pattern, Self)>, + }, /// A value constructor local to the current module, e.g. `Just` and `Ok`. LocalConstructor { /// The source span for this expression. @@ -193,6 +212,7 @@ impl Expression { } } Self::If { output_type, .. } => output_type.clone(), + Self::Match { match_type, .. } => match_type.clone(), Self::LocalConstructor { constructor_type, .. } => constructor_type.clone(), @@ -220,6 +240,7 @@ impl Expression { Self::Function { span, .. } => *span, Self::Call { span, .. } => *span, Self::If { span, .. } => *span, + Self::Match { span, .. } => *span, Self::LocalConstructor { span, .. } => *span, Self::ImportedConstructor { span, .. } => *span, Self::LocalVariable { span, .. } => *span, @@ -296,3 +317,33 @@ impl FunctionBinder { } } } + +/// A pattern to be matched. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum Pattern { + /// A local constructor pattern. + LocalConstructor { + /// The source span for this pattern. + span: Span, + /// `Just` + constructor: ProperName, + /// Pattern arguments to the constructor. + arguments: Vec, + }, + /// An importedf constructor pattern. + ImportedConstructor { + /// The source span for this pattern. + span: Span, + /// `Maybe.Just` + constructor: FullyQualifiedProperName, + /// Pattern arguments to the constructor. + arguments: Vec, + }, + /// A variable binding pattern. + Variable { + /// The source span for this pattern. + span: Span, + /// Name to bind. + name: Name, + }, +} diff --git a/crates/ditto-checker/golden-tests/type-errors/match_unification_error_0.ditto b/crates/ditto-checker/golden-tests/type-errors/match_unification_error_0.ditto new file mode 100644 index 000000000..a5896f7bc --- /dev/null +++ b/crates/ditto-checker/golden-tests/type-errors/match_unification_error_0.ditto @@ -0,0 +1,8 @@ +module Test exports (..); + +type Foo = Foo; +type Bar = Bar; + +nah = + match Foo with + | Bar -> unit; diff --git a/crates/ditto-checker/golden-tests/type-errors/match_unification_error_0.error b/crates/ditto-checker/golden-tests/type-errors/match_unification_error_0.error new file mode 100644 index 000000000..21d07f1ee --- /dev/null +++ b/crates/ditto-checker/golden-tests/type-errors/match_unification_error_0.error @@ -0,0 +1,12 @@ + + × types don't unify + ╭─[golden:5:1] + 5 │ + 6 │ nah = + 7 │ match Foo with + 8 │ | Bar -> unit; + · ─┬─ + · ╰── here + ╰──── + help: expected Foo + got Bar diff --git a/crates/ditto-checker/golden-tests/type-errors/match_unification_error_1.ditto b/crates/ditto-checker/golden-tests/type-errors/match_unification_error_1.ditto new file mode 100644 index 000000000..290263163 --- /dev/null +++ b/crates/ditto-checker/golden-tests/type-errors/match_unification_error_1.ditto @@ -0,0 +1,10 @@ +module Test exports (..); + +type Maybe(a) = Just(a) | Nothing; +type Foo = Foo; +type Bar = Bar; + +nah = (maybe_foo : Maybe(Foo)) -> + match maybe_foo with + | Just(Bar) -> unit + | Nothing -> unit; diff --git a/crates/ditto-checker/golden-tests/type-errors/match_unification_error_1.error b/crates/ditto-checker/golden-tests/type-errors/match_unification_error_1.error new file mode 100644 index 000000000..775e8cd1b --- /dev/null +++ b/crates/ditto-checker/golden-tests/type-errors/match_unification_error_1.error @@ -0,0 +1,13 @@ + + × types don't unify + ╭─[golden:6:1] + 6 │ + 7 │ nah = (maybe_foo : Maybe(Foo)) -> + 8 │ match maybe_foo with + 9 │ | Just(Bar) -> unit + · ─┬─ + · ╰── here + 10 │ | Nothing -> unit; + ╰──── + help: expected Foo + got Bar diff --git a/crates/ditto-checker/golden-tests/type-errors/match_unification_error_2.ditto b/crates/ditto-checker/golden-tests/type-errors/match_unification_error_2.ditto new file mode 100644 index 000000000..a26cf17c9 --- /dev/null +++ b/crates/ditto-checker/golden-tests/type-errors/match_unification_error_2.ditto @@ -0,0 +1,8 @@ +module Test exports (..); + +type Maybe(a) = Just(a) | Nothing; + +nah = (maybe_foo : Maybe(Int)) -> + match maybe_foo with + | Just(a, b) -> unit + | Nothing -> unit; diff --git a/crates/ditto-checker/golden-tests/type-errors/match_unification_error_2.error b/crates/ditto-checker/golden-tests/type-errors/match_unification_error_2.error new file mode 100644 index 000000000..b6a873095 --- /dev/null +++ b/crates/ditto-checker/golden-tests/type-errors/match_unification_error_2.error @@ -0,0 +1,11 @@ + + × wrong number of arguments + ╭─[golden:4:1] + 4 │ + 5 │ nah = (maybe_foo : Maybe(Int)) -> + 6 │ match maybe_foo with + 7 │ | Just(a, b) -> unit + · ─────┬──── + · ╰── this expects 1 argument + 8 │ | Nothing -> unit; + ╰──── diff --git a/crates/ditto-checker/golden-tests/type-errors/match_unification_error_3.ditto b/crates/ditto-checker/golden-tests/type-errors/match_unification_error_3.ditto new file mode 100644 index 000000000..372529155 --- /dev/null +++ b/crates/ditto-checker/golden-tests/type-errors/match_unification_error_3.ditto @@ -0,0 +1,8 @@ +module Test exports (..); + +type Maybe(a) = Just(a) | Nothing; + +nah = (maybe_foo : Maybe(Int)) -> + match maybe_foo with + | Just -> unit + | Nothing -> unit; diff --git a/crates/ditto-checker/golden-tests/type-errors/match_unification_error_3.error b/crates/ditto-checker/golden-tests/type-errors/match_unification_error_3.error new file mode 100644 index 000000000..06dda50bb --- /dev/null +++ b/crates/ditto-checker/golden-tests/type-errors/match_unification_error_3.error @@ -0,0 +1,11 @@ + + × wrong number of arguments + ╭─[golden:4:1] + 4 │ + 5 │ nah = (maybe_foo : Maybe(Int)) -> + 6 │ match maybe_foo with + 7 │ | Just -> unit + · ──┬─ + · ╰── this expects 1 argument + 8 │ | Nothing -> unit; + ╰──── diff --git a/crates/ditto-checker/golden-tests/type-errors/match_unification_error_4.ditto b/crates/ditto-checker/golden-tests/type-errors/match_unification_error_4.ditto new file mode 100644 index 000000000..7bd977136 --- /dev/null +++ b/crates/ditto-checker/golden-tests/type-errors/match_unification_error_4.ditto @@ -0,0 +1,8 @@ +module Test exports (..); + +type Maybe(a) = Just(a) | Nothing; + +nah = (maybe_foo : Maybe(Int)) -> + match maybe_foo with + | Just(n) -> unit + | Nothing(a) -> unit; diff --git a/crates/ditto-checker/golden-tests/type-errors/match_unification_error_4.error b/crates/ditto-checker/golden-tests/type-errors/match_unification_error_4.error new file mode 100644 index 000000000..e5df2f75e --- /dev/null +++ b/crates/ditto-checker/golden-tests/type-errors/match_unification_error_4.error @@ -0,0 +1,10 @@ + + × wrong number of arguments + ╭─[golden:5:1] + 5 │ nah = (maybe_foo : Maybe(Int)) -> + 6 │ match maybe_foo with + 7 │ | Just(n) -> unit + 8 │ | Nothing(a) -> unit; + · ─────┬──── + · ╰── this expects no arguments + ╰──── diff --git a/crates/ditto-checker/golden-tests/type-errors/match_unification_error_5.ditto b/crates/ditto-checker/golden-tests/type-errors/match_unification_error_5.ditto new file mode 100644 index 000000000..dbafb37c4 --- /dev/null +++ b/crates/ditto-checker/golden-tests/type-errors/match_unification_error_5.ditto @@ -0,0 +1,8 @@ +module Test exports (..); + +type Maybe(a) = Just(a) | Nothing; + +nah = (maybe_foo : Maybe(Int)): Int -> + match maybe_foo with + | Just(n) -> unit + | Nothing -> unit; diff --git a/crates/ditto-checker/golden-tests/type-errors/match_unification_error_5.error b/crates/ditto-checker/golden-tests/type-errors/match_unification_error_5.error new file mode 100644 index 000000000..771dce0a9 --- /dev/null +++ b/crates/ditto-checker/golden-tests/type-errors/match_unification_error_5.error @@ -0,0 +1,13 @@ + + × types don't unify + ╭─[golden:4:1] + 4 │ + 5 │ nah = (maybe_foo : Maybe(Int)): Int -> + 6 │ match maybe_foo with + 7 │ | Just(n) -> unit + · ──┬─ + · ╰── here + 8 │ | Nothing -> unit; + ╰──── + help: expected Int + got Unit diff --git a/crates/ditto-checker/src/module/value_declarations/mod.rs b/crates/ditto-checker/src/module/value_declarations/mod.rs index 1f7e7ff9a..a89158825 100644 --- a/crates/ditto-checker/src/module/value_declarations/mod.rs +++ b/crates/ditto-checker/src/module/value_declarations/mod.rs @@ -455,6 +455,18 @@ fn toposort_value_declarations( get_connected_nodes_rec(true_clause, nodes, accum); get_connected_nodes_rec(false_clause, nodes, accum); } + Expression::Match { + expression, + head_arm, + tail_arms, + .. + } => { + get_connected_nodes_rec(expression, nodes, accum); + get_connected_nodes_rec(&head_arm.expression, nodes, accum); + for tail_arm in tail_arms.iter() { + get_connected_nodes_rec(&tail_arm.expression, nodes, accum); + } + } Expression::Array(elements) => { if let Some(ref elements) = elements.value { elements.iter().for_each(|element| { diff --git a/crates/ditto-checker/src/typechecker/env.rs b/crates/ditto-checker/src/typechecker/env.rs index 9ab390ee1..8edd98734 100644 --- a/crates/ditto-checker/src/typechecker/env.rs +++ b/crates/ditto-checker/src/typechecker/env.rs @@ -1,15 +1,15 @@ use super::{common::type_variables, Scheme}; use crate::supply::Supply; use ditto_ast::{ - Expression, FullyQualifiedName, FullyQualifiedProperName, Name, ProperName, QualifiedName, - QualifiedProperName, Span, Type, + Expression, FullyQualifiedName, FullyQualifiedProperName, Name, Pattern, ProperName, + QualifiedName, QualifiedProperName, Span, Type, }; use std::{ collections::{HashMap, HashSet}, default::Default, }; -#[derive(Default)] +#[derive(Default, Clone)] pub struct Env { pub constructors: EnvConstructors, pub values: EnvValues, @@ -157,6 +157,26 @@ impl EnvConstructor { } } + // REVIEW should this be `into_pattern`? + pub fn to_pattern(&self, span: Span, arguments: Vec) -> Pattern { + match self { + Self::ModuleConstructor { constructor, .. } => Pattern::LocalConstructor { + span, + constructor: constructor.clone(), + arguments, + }, + Self::ImportedConstructor { constructor, .. } => Pattern::ImportedConstructor { + span, + constructor: constructor.clone(), + arguments, + }, + } + } + + pub fn get_type(&self, supply: &mut Supply) -> Type { + self.get_scheme().instantiate(supply) + } + fn get_scheme(&self) -> Scheme { match self { Self::ModuleConstructor { diff --git a/crates/ditto-checker/src/typechecker/mod.rs b/crates/ditto-checker/src/typechecker/mod.rs index ce2a4ae2d..0cb1cdbf5 100644 --- a/crates/ditto-checker/src/typechecker/mod.rs +++ b/crates/ditto-checker/src/typechecker/mod.rs @@ -19,7 +19,7 @@ use crate::{ result::{Result, TypeError, Warning, Warnings}, supply::Supply, }; -use ditto_ast::{unqualified, Argument, Expression, FunctionBinder, PrimType, Span, Type}; +use ditto_ast::{unqualified, Argument, Expression, FunctionBinder, Pattern, PrimType, Span, Type}; use ditto_cst as cst; use std::collections::HashSet; @@ -378,6 +378,11 @@ pub fn infer(env: &Env, state: &mut State, expr: pre::Expression) -> Result infer_or_check_match(env, state, span, expression, arms, None), } } @@ -387,16 +392,170 @@ pub fn check( expected: Type, expr: pre::Expression, ) -> Result { - let expression = infer(env, state, expr)?; - unify( + match expr { + pre::Expression::Match { + span, + box expression, + arms, + } => infer_or_check_match(env, state, span, expression, arms, Some(expected)), + _ => { + let expression = infer(env, state, expr)?; + unify( + state, + expression.get_span(), + Constraint { + expected, + actual: expression.get_type(), + }, + )?; + Ok(expression) + } + } +} + +fn infer_or_check_match( + env: &Env, + state: &mut State, + span: Span, + expression: pre::Expression, + arms: non_empty_vec::NonEmpty<(pre::Pattern, pre::Expression)>, + match_type: Option, +) -> Result { + let expression = infer(env, state, expression)?; + let pattern_type = expression.get_type(); + + let (head_arm, tail_arms) = arms.split_first(); + let mut head_arm_env = env.clone(); + let head_arm_pattern = check_pattern( + &mut head_arm_env, state, - expression.get_span(), - Constraint { - expected, - actual: expression.get_type(), - }, + pattern_type.clone(), + head_arm.0.clone(), )?; - Ok(expression) + + let (head_arm_expression, match_type) = if let Some(expected) = match_type { + let head_arm_expression = + check(&head_arm_env, state, expected.clone(), head_arm.1.clone())?; + (head_arm_expression, expected) + } else { + let head_arm_expression = infer(&head_arm_env, state, head_arm.1.clone())?; + let match_type = head_arm_expression.get_type(); + (head_arm_expression, match_type) + }; + + let mut arms = non_empty_vec::NonEmpty::new((head_arm_pattern, head_arm_expression)); + for tail_arm in tail_arms { + let mut tail_arm_env = env.clone(); + let tail_arm_pattern = check_pattern( + &mut tail_arm_env, + state, + pattern_type.clone(), + tail_arm.0.clone(), + )?; + let tail_arm_expression = + check(&tail_arm_env, state, match_type.clone(), tail_arm.1.clone())?; + arms.push((tail_arm_pattern, tail_arm_expression)); + } + + Ok(Expression::Match { + span, + match_type, + expression: Box::new(expression), + arms, + }) +} + +fn check_pattern( + env: &mut Env, + state: &mut State, + expected: Type, + pattern: pre::Pattern, +) -> Result { + match pattern { + pre::Pattern::Constructor { + span, + constructor, + arguments, + } => { + if let Some(count) = state.constructor_references.get_mut(&constructor) { + *count += 1 + } else { + state.constructor_references.insert(constructor.clone(), 1); + } + + let env_constructors = env.constructors.clone(); + let env_constructor = env_constructors.get(&constructor).ok_or_else(|| { + TypeError::UnknownConstructor { + span, + constructor, + ctors_in_scope: env_constructors.keys().cloned().collect(), + } + })?; + + let constructor_type = env_constructor.get_type(&mut state.supply); + + let arguments_len = arguments.len(); + let constraint = match constructor_type.clone() { + Type::Function { + parameters, + box return_type, + .. + } => { + let parameters_len = parameters.len(); + if parameters_len != arguments_len { + // TODO reusing this type error is a bit lazy, + // might be worth adding `PatternArgumentLengthMismatch`? + return Err(TypeError::ArgumentLengthMismatch { + function_span: span, + wanted: parameters_len, + got: arguments_len, + }); + } + Constraint { + expected, + actual: return_type, + } + } + actual => { + if arguments_len != 0 { + // TODO reusing this type error is a bit lazy, + // might be worth adding `PatternArgumentLengthMismatch`? + return Err(TypeError::ArgumentLengthMismatch { + function_span: span, + wanted: 0, + got: arguments_len, + }); + } + Constraint { expected, actual } + } + }; + + unify(state, span, constraint)?; + + if let Type::Function { parameters, .. } = state.substitution.apply(constructor_type) { + let mut checked_arguments = Vec::new(); + for (parameter, argument) in parameters.into_iter().zip(arguments) { + let checked_argument = check_pattern(env, state, parameter, argument)?; + checked_arguments.push(checked_argument); + } + Ok(env_constructor.to_pattern(span, checked_arguments)) + } else { + Ok(env_constructor.to_pattern(span, vec![])) + } + } + pre::Pattern::Variable { span, name } => { + let variable_scheme = env.generalize(expected); + env.values.insert( + unqualified(name.clone()), + EnvValue::ModuleValue { + span, + variable_scheme, + variable: name.clone(), + }, + ); + Ok(Pattern::Variable { span, name }) + } + } } #[derive(Debug)] diff --git a/crates/ditto-checker/src/typechecker/pre_ast.rs b/crates/ditto-checker/src/typechecker/pre_ast.rs index bcb93958d..acd465a52 100644 --- a/crates/ditto-checker/src/typechecker/pre_ast.rs +++ b/crates/ditto-checker/src/typechecker/pre_ast.rs @@ -10,8 +10,10 @@ use crate::{ }; use ditto_ast::{Kind, Name, QualifiedName, QualifiedProperName, Span, Type}; use ditto_cst as cst; +use non_empty_vec::NonEmpty; use std::collections::hash_map; +#[derive(Clone)] // FIXME: we really shouldn't have to clone this... pub enum Expression { Function { span: Span, @@ -34,6 +36,11 @@ pub enum Expression { span: Span, constructor: QualifiedProperName, }, + Match { + span: Span, + expression: Box, + arms: NonEmpty<(Pattern, Self)>, + }, Variable { span: Span, variable: QualifiedName, @@ -65,6 +72,7 @@ pub enum Expression { }, } +#[derive(Clone)] pub enum FunctionBinder { Name { span: Span, @@ -73,6 +81,7 @@ pub enum FunctionBinder { }, } +#[derive(Clone)] pub enum Argument { Expression(Expression), } @@ -154,6 +163,27 @@ fn convert_cst( span, constructor: QualifiedProperName::from(ctor), }), + cst::Expression::Match { + box expression, + head_arm, + tail_arms, + .. + } => { + let expression = convert_cst(env, state, expression)?; + let head_arm_pattern = Pattern::from(head_arm.pattern); + let head_arm_expression = convert_cst(env, state, *head_arm.expression)?; + let mut arms = NonEmpty::new((head_arm_pattern, head_arm_expression)); + for tail_arm in tail_arms.into_iter() { + let tail_arm_pattern = Pattern::from(tail_arm.pattern); + let tail_arm_expression = convert_cst(env, state, *tail_arm.expression)?; + arms.push((tail_arm_pattern, tail_arm_expression)); + } + Ok(Expression::Match { + span, + expression: Box::new(expression), + arms, + }) + } cst::Expression::Unit { .. } => Ok(Expression::Unit { span }), cst::Expression::True { .. } => Ok(Expression::True { span }), cst::Expression::False { .. } => Ok(Expression::False { span }), @@ -345,6 +375,21 @@ fn substitute_type_annotations(subst: &Substitution, expression: Expression) -> true_clause: Box::new(substitute_type_annotations(subst, true_clause)), false_clause: Box::new(substitute_type_annotations(subst, false_clause)), }, + Match { + span, + box expression, + arms, + } => Match { + span, + expression: Box::new(substitute_type_annotations(subst, expression)), + arms: unsafe { + NonEmpty::new_unchecked( + arms.into_iter() + .map(|(pattern, expr)| (pattern, expr)) + .collect(), + ) + }, + }, Constructor { span, constructor } => Constructor { span, constructor }, Variable { span, variable } => Variable { span, variable }, String { span, value } => String { span, value }, @@ -363,6 +408,48 @@ fn substitute_type_annotations(subst: &Substitution, expression: Expression) -> } } +#[derive(Clone)] +pub enum Pattern { + Constructor { + span: Span, + constructor: QualifiedProperName, + arguments: Vec, + }, + Variable { + span: Span, + name: Name, + }, +} + +impl From for Pattern { + fn from(cst_pattern: cst::Pattern) -> Self { + let span = cst_pattern.get_span(); + match cst_pattern { + cst::Pattern::NullaryConstructor { constructor } => Pattern::Constructor { + span, + constructor: QualifiedProperName::from(constructor), + arguments: vec![], + }, + cst::Pattern::Constructor { + constructor, + arguments, + } => Pattern::Constructor { + span, + constructor: QualifiedProperName::from(constructor), + arguments: arguments + .value + .into_iter() + .map(|box pat| Self::from(pat)) + .collect(), + }, + cst::Pattern::Variable { name } => Pattern::Variable { + span, + name: Name::from(name), + }, + } + } +} + fn strip_number_separators(value: String) -> String { value.replace('_', "") } diff --git a/crates/ditto-checker/src/typechecker/substitution.rs b/crates/ditto-checker/src/typechecker/substitution.rs index 26fde24af..f9ee8d6d5 100644 --- a/crates/ditto-checker/src/typechecker/substitution.rs +++ b/crates/ditto-checker/src/typechecker/substitution.rs @@ -126,6 +126,23 @@ impl Substitution { true_clause: Box::new(self.apply_expression(true_clause)), false_clause: Box::new(self.apply_expression(false_clause)), }, + Match { + span, + match_type, + box expression, + arms, + } => Match { + span, + match_type: self.apply(match_type), + expression: Box::new(self.apply_expression(expression)), + arms: unsafe { + NonEmpty::new_unchecked( + arms.into_iter() + .map(|(pattern, expr)| (pattern, self.apply_expression(expr))) + .collect(), + ) + }, + }, LocalConstructor { constructor_type, span, diff --git a/crates/ditto-checker/src/typechecker/tests/match.rs b/crates/ditto-checker/src/typechecker/tests/match.rs new file mode 100644 index 000000000..290b087e7 --- /dev/null +++ b/crates/ditto-checker/src/typechecker/tests/match.rs @@ -0,0 +1,17 @@ +use super::macros::*; +use crate::TypeError::*; + +#[test] +fn it_typechecks_as_expected() { + assert_type!(r#" match 5 with | x -> 2.0 "#, "Float"); + assert_type!(r#" match true with | x -> x "#, "Bool"); + assert_type!(r#" match true with | x -> unit | y -> unit "#, "Unit"); +} + +#[test] +fn it_errors_as_expected() { + assert_type_error!( + r#" match 5 with | x -> unit | y -> true "#, + TypesNotEqual { .. } + ); +} diff --git a/crates/ditto-checker/src/typechecker/tests/mod.rs b/crates/ditto-checker/src/typechecker/tests/mod.rs index 29b567ecd..7bd47bd14 100644 --- a/crates/ditto-checker/src/typechecker/tests/mod.rs +++ b/crates/ditto-checker/src/typechecker/tests/mod.rs @@ -6,5 +6,6 @@ mod float; mod function; mod int; pub(self) mod macros; +mod r#match; mod string; mod unit; diff --git a/crates/ditto-codegen-js/golden-tests/javascript/match_expressions.ditto b/crates/ditto-codegen-js/golden-tests/javascript/match_expressions.ditto new file mode 100644 index 000000000..d586e801a --- /dev/null +++ b/crates/ditto-codegen-js/golden-tests/javascript/match_expressions.ditto @@ -0,0 +1,9 @@ +module Test exports (..); + +type Maybe(a) = Just(a) | Nothing; + + +with_default = (maybe: Maybe(a), default: a): a -> + match maybe with + | Just(a) -> a + | Nothing -> default; diff --git a/crates/ditto-codegen-js/golden-tests/javascript/match_expressions.js b/crates/ditto-codegen-js/golden-tests/javascript/match_expressions.js new file mode 100644 index 000000000..a98562c82 --- /dev/null +++ b/crates/ditto-codegen-js/golden-tests/javascript/match_expressions.js @@ -0,0 +1,17 @@ +function Just($0) { + return ["Just", $0]; +} +const Nothing = ["Nothing"]; +function withDefault(maybe, $default) { + return maybe[0] === "Nothing" + ? $default + : maybe[0] === "Just" + ? (() => { + const a = maybe[1]; + return a; + })() + : (() => { + throw new Error("Pattern match error"); + })(); +} +export { Just, Nothing, withDefault }; diff --git a/crates/ditto-codegen-js/src/ast.rs b/crates/ditto-codegen-js/src/ast.rs index 1dea349ae..ad68e6e1a 100644 --- a/crates/ditto-codegen-js/src/ast.rs +++ b/crates/ditto-codegen-js/src/ast.rs @@ -46,16 +46,26 @@ pub enum ModuleStatement { } /// A bunch of statements surrounded by braces. +#[derive(Clone)] pub struct Block(pub Vec); /// A single JavaScript statement. /// /// These end with a semicolon. +#[derive(Clone)] pub enum BlockStatement { /// ```javascript /// const ident = expression; /// ``` - _ConstAssignment { ident: Ident, value: Expression }, + ConstAssignment { ident: Ident, value: Expression }, + /// ```javascript + /// console.log("hi"); + /// ``` + _Expression(Expression), + /// ```javascript + /// throw new Error("message") + /// ``` + Throw(String), /// ```javascript /// return bar; /// return; @@ -63,6 +73,7 @@ pub enum BlockStatement { Return(Option), } +#[derive(Clone)] pub enum Expression { /// `true` True, @@ -86,22 +97,22 @@ pub enum Expression { /// function(argument, argument, argument) /// ``` Call { - function: Box, - arguments: Vec, + function: Box, + arguments: Vec, }, /// ```javascript /// condition ? true_clause : false_clause /// ``` Conditional { - condition: Box, - true_clause: Box, - false_clause: Box, + condition: Box, + true_clause: Box, + false_clause: Box, }, /// ```javascript /// [] /// [5, 5, 5] /// ``` - Array(Vec), + Array(Vec), /// ```javascript /// 5 /// 5.0 @@ -115,9 +126,36 @@ pub enum Expression { /// undefined /// ``` Undefined, + /// IIFE + /// + /// ```javascript + /// (() => { block })() + /// ``` + Block(Block), + /// ```javascript + /// 1 + 2 + /// x && y + /// ``` + Operator { + op: Operator, + lhs: Box, + rhs: Box, + }, + IndexAccess { + target: Box, + index: Box, + }, +} + +/// A binary operator. +#[derive(Clone)] +pub enum Operator { + And, + Equals, } /// The _body_ of an arrow function. +#[derive(Clone)] pub enum ArrowFunctionBody { /// ```javascript /// () => expression; @@ -126,5 +164,5 @@ pub enum ArrowFunctionBody { /// ```javascript /// () => { block } /// ``` - _Block(Block), + Block(Block), } diff --git a/crates/ditto-codegen-js/src/convert.rs b/crates/ditto-codegen-js/src/convert.rs index 8e5fda4da..aa3d3f836 100644 --- a/crates/ditto-codegen-js/src/convert.rs +++ b/crates/ditto-codegen-js/src/convert.rs @@ -1,6 +1,6 @@ use crate::ast::{ ArrowFunctionBody, Block, BlockStatement, Expression, Ident, ImportStatement, Module, - ModuleStatement, + ModuleStatement, Operator, }; use convert_case::{Case, Casing}; use ditto_ast::graph::Scc; @@ -300,6 +300,112 @@ fn convert_expression( ditto_ast::Expression::True { .. } => Expression::True, ditto_ast::Expression::False { .. } => Expression::False, ditto_ast::Expression::Unit { .. } => Expression::Undefined, // REVIEW could use `null` or `null` here? + ditto_ast::Expression::Match { + span: _, + box expression, + arms, + .. + } => { + let expression = convert_expression(imported_idents, expression); + let err = Expression::Block(Block(vec![BlockStatement::Throw(String::from( + // TODO: mention the file location here? + "Pattern match error", + ))])); + arms.into_iter() + .fold(err, |false_clause, (pattern, arm_expression)| { + let (condition, assignments) = convert_pattern(expression.clone(), pattern); + + let expression = if assignments.is_empty() { + convert_expression(imported_idents, arm_expression) + } else { + let mut block_statements = assignments + .into_iter() + .map(|(ident, value)| BlockStatement::ConstAssignment { ident, value }) + .collect::>(); + let arm_expression = convert_expression(imported_idents, arm_expression); + block_statements.push(BlockStatement::Return(Some(arm_expression))); + Expression::Block(Block(block_statements)) + }; + + if let Some(condition) = condition { + Expression::Conditional { + condition: Box::new(condition), + true_clause: Box::new(expression), + false_clause: Box::new(false_clause), + } + } else { + expression + } + }) + } + } +} + +type Assignment = (Ident, Expression); +type Assignments = Vec; + +fn convert_pattern( + expression: Expression, + pattern: ditto_ast::Pattern, +) -> (Option, Assignments) { + let mut conditions = Vec::new(); + let mut assignments = Vec::new(); + convert_pattern_rec(expression, pattern, &mut conditions, &mut assignments); + if let Some((condition, conditions)) = conditions.split_first() { + let condition = + conditions + .iter() + .fold(condition.clone(), |rhs, lhs| Expression::Operator { + op: Operator::And, + lhs: Box::new(lhs.clone()), + rhs: Box::new(rhs), + }); + (Some(condition), assignments) + } else { + (None, assignments) + } +} + +fn convert_pattern_rec( + expression: Expression, + pattern: ditto_ast::Pattern, + conditions: &mut Vec, + assignments: &mut Vec, +) { + match pattern { + ditto_ast::Pattern::Variable { name, .. } => { + let assignment = (name.into(), expression); + assignments.push(assignment) + } + ditto_ast::Pattern::LocalConstructor { + constructor, + arguments, + .. + } => { + let condition = Expression::Operator { + op: Operator::Equals, + lhs: Box::new(Expression::IndexAccess { + target: Box::new(expression.clone()), + index: Box::new(Expression::Number(String::from("0"))), + }), + rhs: Box::new(Expression::String(constructor.0)), + }; + conditions.push(condition); + for (i, pattern) in arguments.into_iter().enumerate() { + let expression = Expression::IndexAccess { + target: Box::new(expression.clone()), + index: Box::new(Expression::Number((i + 1).to_string())), + }; + convert_pattern_rec(expression, pattern, conditions, assignments); + } + } + ditto_ast::Pattern::ImportedConstructor { + constructor: _, + arguments: _, + .. + } => { + todo!(); + } } } diff --git a/crates/ditto-codegen-js/src/render.rs b/crates/ditto-codegen-js/src/render.rs index 0ee74ec55..29d9c7059 100644 --- a/crates/ditto-codegen-js/src/render.rs +++ b/crates/ditto-codegen-js/src/render.rs @@ -1,6 +1,6 @@ use crate::ast::{ ArrowFunctionBody, Block, BlockStatement, Expression, Ident, ImportStatement, Module, - ModuleStatement, + ModuleStatement, Operator, }; pub fn render_module(module: Module) -> String { @@ -113,7 +113,16 @@ impl Render for BlockStatement { expression.render(accum); accum.push(';'); } - Self::_ConstAssignment { ident, value } => { + Self::_Expression(expression) => { + expression.render(accum); + accum.push(';'); + } + Self::Throw(message) => { + accum.push_str("throw new Error(\""); + accum.push_str(message); + accum.push_str("\");"); + } + Self::ConstAssignment { ident, value } => { accum.push_str(&format!("const {ident} = ", ident = ident.0)); value.render(accum); accum.push(';'); @@ -204,6 +213,33 @@ impl Render for Expression { Self::Undefined => { accum.push_str("undefined"); } + Self::Block(block) => { + // IIFE + accum.push('('); + let arrow_function = Self::ArrowFunction { + parameters: Vec::new(), + body: Box::new(ArrowFunctionBody::Block(block.clone())), + }; + arrow_function.render(accum); + accum.push_str(")()"); + } + Self::Operator { op, lhs, rhs } => { + // Always use parens rather than worry about precedence + accum.push('('); + lhs.render(accum); + accum.push_str(match op { + Operator::And => " && ", + Operator::Equals => " === ", + }); + rhs.render(accum); + accum.push(')'); + } + Self::IndexAccess { target, index } => { + target.render(accum); + accum.push('['); + index.render(accum); + accum.push(']'); + } } } } @@ -211,7 +247,7 @@ impl Render for Expression { impl Render for ArrowFunctionBody { fn render(&self, accum: &mut String) { match self { - Self::_Block(block) => block.render(accum), + Self::Block(block) => block.render(accum), Self::Expression(expression) => expression.render(accum), } } @@ -257,7 +293,7 @@ mod tests { assert_render!( Expression::ArrowFunction { parameters: vec![ident!("a")], - body: Box::new(ArrowFunctionBody::_Block(Block(vec![ + body: Box::new(ArrowFunctionBody::Block(Block(vec![ BlockStatement::Return(Some(Expression::String("hello".to_string()))) ]))), }, @@ -334,6 +370,29 @@ mod tests { }, "(true?true:false)?false?0:1:false?2:3" ); + assert_render!( + Expression::Operator { + op: Operator::Equals, + lhs: Box::new(Expression::Operator { + op: Operator::And, + lhs: Box::new(Expression::False), + rhs: Box::new(Expression::True), + }), + rhs: Box::new(Expression::True), + }, + "((false && true) === true)" + ); + + assert_render!( + Expression::IndexAccess { + target: Box::new(Expression::IndexAccess { + target: Box::new(Expression::Variable(ident!("foo"))), + index: Box::new(Expression::String(String::from("bar"))) + }), + index: Box::new(Expression::String(String::from("baz"))) + }, + r#"foo["bar"]["baz"]"# + ); } #[test] @@ -343,6 +402,10 @@ mod tests { "return true;" ); assert_render!(BlockStatement::Return(None), "return;"); + assert_render!( + BlockStatement::Throw(String::from("aaaahhh")), + "throw new Error(\"aaaahhh\");" + ) } #[test] diff --git a/crates/ditto-cst/src/expression.rs b/crates/ditto-cst/src/expression.rs index 324b937be..62257dc80 100644 --- a/crates/ditto-cst/src/expression.rs +++ b/crates/ditto-cst/src/expression.rs @@ -1,7 +1,7 @@ use crate::{ - BracketsList, Colon, ElseKeyword, FalseKeyword, IfKeyword, Name, Parens, ParensList, - QualifiedName, QualifiedProperName, RightArrow, StringToken, ThenKeyword, TrueKeyword, Type, - UnitKeyword, + BracketsList, Colon, ElseKeyword, FalseKeyword, IfKeyword, MatchKeyword, Name, Parens, + ParensList, ParensList1, Pipe, QualifiedName, QualifiedProperName, RightArrow, StringToken, + ThenKeyword, TrueKeyword, Type, UnitKeyword, WithKeyword, }; /// A value expression. @@ -54,6 +54,24 @@ pub enum Expression { /// The expression to evaluate otherwise. false_clause: Box, }, + /// A pattern match. + /// + /// ```ditto + /// match some_expr with + /// | Pattern -> another_expr + /// ``` + Match { + /// `match` + match_keyword: MatchKeyword, + /// Expression to be matched. + expression: Box, + /// `with` + with_keyword: WithKeyword, + /// The first match arm (there should be at least one). + head_arm: MatchArm, + /// More match arms. + tail_arms: Vec, + }, /// A value constructor, e.g. `Just` and `Ok`. Constructor(QualifiedProperName), /// A variable. Useful for not repeating things. @@ -88,6 +106,45 @@ pub enum Expression { Array(BracketsList>), } +/// A single arm of a `match` expression. +/// +/// ```ditto +/// | Pattern -> expression +/// ``` +#[derive(Debug, Clone)] +pub struct MatchArm { + /// `|` + pub pipe: Pipe, + /// Pattern to be matched. + pub pattern: Pattern, + /// `->` + pub right_arrow: RightArrow, + /// The expression to return if the pattern is matched. + pub expression: Box, +} + +/// A pattern to be matched. +#[derive(Debug, Clone)] +pub enum Pattern { + /// A constructor pattern without arguments. + NullaryConstructor { + /// `Maybe.Just` + constructor: QualifiedProperName, + }, + /// A constructor pattern with arguments. + Constructor { + /// `Maybe.Just` + constructor: QualifiedProperName, + /// Pattern arguments to the constructor. + arguments: ParensList1>, + }, + /// A variable binding pattern. + Variable { + /// Name to bind. + name: Name, + }, +} + /// `: String` #[derive(Debug, Clone)] pub struct TypeAnnotation(pub Colon, pub Type); diff --git a/crates/ditto-cst/src/get_span.rs b/crates/ditto-cst/src/get_span.rs index 74f221df0..fd1c4bab7 100644 --- a/crates/ditto-cst/src/get_span.rs +++ b/crates/ditto-cst/src/get_span.rs @@ -1,6 +1,6 @@ use crate::{ - Brackets, Expression, ModuleName, Name, PackageName, Parens, ProperName, QualifiedName, - QualifiedProperName, Span, Token, Type, TypeAnnotation, TypeCallFunction, + Brackets, Expression, ModuleName, Name, PackageName, Parens, Pattern, ProperName, + QualifiedName, QualifiedProperName, Span, Token, Type, TypeAnnotation, TypeCallFunction, }; impl Token { @@ -71,6 +71,19 @@ impl Expression { Self::Parens(parens) => parens.get_span(), Self::Variable(qualified_name) => qualified_name.get_span(), Self::Constructor(qualified_proper_name) => qualified_proper_name.get_span(), + Self::Match { + match_keyword, + head_arm, + tail_arms, + .. + } => { + let start = match_keyword.0.get_span(); + if let Some(last_arm) = tail_arms.last() { + start.merge(&last_arm.expression.get_span()) + } else { + start.merge(&head_arm.expression.get_span()) + } + } Self::Call { function, arguments, @@ -158,3 +171,17 @@ impl Brackets { .merge(&self.close_bracket.0.get_span()) } } + +impl Pattern { + /// Get the source span. + pub fn get_span(&self) -> Span { + match self { + Self::NullaryConstructor { constructor } => constructor.get_span(), + Self::Constructor { + constructor, + arguments, + } => constructor.get_span().merge(&arguments.get_span()), + Self::Variable { name } => name.get_span(), + } + } +} diff --git a/crates/ditto-cst/src/parser/expression.rs b/crates/ditto-cst/src/parser/expression.rs index 3c2ab8a4a..8e3657771 100644 --- a/crates/ditto-cst/src/parser/expression.rs +++ b/crates/ditto-cst/src/parser/expression.rs @@ -1,8 +1,9 @@ use super::{parse_rule, Result, Rule}; use crate::{ - BracketsList, Colon, ElseKeyword, Expression, FalseKeyword, IfKeyword, Name, Parens, - ParensList, QualifiedName, QualifiedProperName, RightArrow, StringToken, ThenKeyword, - TrueKeyword, Type, TypeAnnotation, UnitKeyword, + BracketsList, Colon, ElseKeyword, Expression, FalseKeyword, IfKeyword, MatchArm, MatchKeyword, + Name, Parens, ParensList, ParensList1, Pattern, Pipe, QualifiedName, QualifiedProperName, + RightArrow, StringToken, ThenKeyword, TrueKeyword, Type, TypeAnnotation, UnitKeyword, + WithKeyword, }; use pest::iterators::Pair; @@ -105,34 +106,94 @@ impl Expression { value: string_token.value[1..string_token.value.len() - 1].to_owned(), ..string_token }; - Expression::String(string_token) + Self::String(string_token) } Rule::expression_array => { let elements = BracketsList::list_from_pair(pair, |expr_pair| { Box::new(Self::from_pair(expr_pair)) }); - Expression::Array(elements) + Self::Array(elements) } Rule::expression_true => { - Expression::True(TrueKeyword::from_pair(pair.into_inner().next().unwrap())) + Self::True(TrueKeyword::from_pair(pair.into_inner().next().unwrap())) } Rule::expression_false => { - Expression::False(FalseKeyword::from_pair(pair.into_inner().next().unwrap())) + Self::False(FalseKeyword::from_pair(pair.into_inner().next().unwrap())) } Rule::expression_unit => { - Expression::Unit(UnitKeyword::from_pair(pair.into_inner().next().unwrap())) + Self::Unit(UnitKeyword::from_pair(pair.into_inner().next().unwrap())) + } + Rule::expression_match => { + let mut inner = pair.into_inner(); + let match_keyword = MatchKeyword::from_pair(inner.next().unwrap()); + let expression = Box::new(Expression::from_pair(inner.next().unwrap())); + let with_keyword = WithKeyword::from_pair(inner.next().unwrap()); + let head_arm = MatchArm::from_pair(inner.next().unwrap()); + let tail_arms = inner.into_iter().map(MatchArm::from_pair).collect(); + Self::Match { + match_keyword, + expression, + with_keyword, + head_arm, + tail_arms, + } } other => unreachable!("{:#?} {:#?}", other, pair.into_inner()), } } } +impl MatchArm { + fn from_pair(pair: Pair) -> Self { + let mut inner = pair.into_inner(); + let pipe = Pipe::from_pair(inner.next().unwrap()); + let pattern = Pattern::from_pair(inner.next().unwrap()); + let right_arrow = RightArrow::from_pair(inner.next().unwrap()); + let expression = Box::new(Expression::from_pair(inner.next().unwrap())); + Self { + pipe, + pattern, + right_arrow, + expression, + } + } +} + +impl Pattern { + fn from_pair(pair: Pair) -> Self { + let mut inner = pair.into_inner(); + let pattern = inner.next().unwrap(); + match pattern.as_rule() { + Rule::pattern_constructor => { + let mut pattern_inner = pattern.into_inner(); + let constructor = QualifiedProperName::from_pair(pattern_inner.next().unwrap()); + if let Some(args) = pattern_inner.next() { + let arguments = ParensList1::list1_from_pair(args, |pair| { + Box::new(Pattern::from_pair(pair)) + }); + return Self::Constructor { + constructor, + arguments, + }; + } + Self::NullaryConstructor { constructor } + } + Rule::pattern_variable => { + let mut pattern_inner = pattern.into_inner(); + let name = Name::from_pair(pattern_inner.next().unwrap()); + Self::Variable { name } + } + other => unreachable!("{:#?} {:#?}", other, pattern.into_inner()), + } + } +} + impl TypeAnnotation { pub(super) fn from_pair(pair: Pair) -> Self { let mut inner = pair.into_inner(); let colon = Colon::from_pair(inner.next().unwrap()); let type_ = Type::from_pair(inner.next().unwrap()); - TypeAnnotation(colon, type_) + Self(colon, type_) } } @@ -366,6 +427,59 @@ mod tests { }) ); } + + #[test] + fn it_parses_match_expressions() { + use crate::{MatchArm, Pattern}; + assert_parses!( + "match x with | foo -> 2", + Expression::Match { + head_arm: MatchArm { + pattern: Pattern::Variable { .. }, + .. + }, + .. + } + ); + assert_parses!( + "match x with | Foo -> 2", + Expression::Match { + head_arm: MatchArm { + pattern: Pattern::NullaryConstructor { .. }, + .. + }, + .. + } + ); + assert_parses!( + "match x with | F.Foo -> 2", + Expression::Match { + head_arm: MatchArm { + pattern: Pattern::NullaryConstructor { .. }, + .. + }, + .. + } + ); + assert_parses!( + "match x with | Foo(bar) -> 2", + Expression::Match { + head_arm: MatchArm { + pattern: Pattern::Constructor { .. }, + .. + }, + .. + } + ); + assert_parses!( + "match x with | Foo(Bar, Baz(bar, Bar)) -> 2", + Expression::Match { .. } + ); + assert_parses!( + "match x with | Foo -> 2 | Bar -> 3", + Expression::Match { tail_arms, .. } if tail_arms.len() == 1 + ); + } } #[cfg(test)] diff --git a/crates/ditto-cst/src/parser/grammar.pest b/crates/ditto-cst/src/parser/grammar.pest index e2a79afc0..29fdba31a 100644 --- a/crates/ditto-cst/src/parser/grammar.pest +++ b/crates/ditto-cst/src/parser/grammar.pest @@ -131,6 +131,7 @@ return_type_annotation = { colon ~ type1 } // used for function expressions only expression = _ { expression_call | expression_function + | expression_match | expression1 } @@ -157,6 +158,10 @@ expression_call = { expression1 ~ expression_call_arguments+ } expression_call_arguments = { open_paren ~ (expression ~ (comma ~ expression)* ~ comma?)? ~ close_paren } +expression_match = { match_keyword ~ expression ~ with_keyword ~ expression_match_arm+ } + +expression_match_arm = { pipe ~ pattern ~ right_arrow ~ expression } + expression_constructor = { qualified_proper_name } expression_function = { expression_function_parameters ~ return_type_annotation? ~ right_arrow ~ expression } @@ -183,6 +188,19 @@ expression_false = { false_keyword } expression_unit = { unit_keyword } +pattern = + { pattern_constructor + | pattern_variable + } + +pattern_constructor = { pattern_constructor_proper_name ~ pattern_constructor_arguments? } + +pattern_constructor_proper_name = { qualified_proper_name } + +pattern_constructor_arguments = { open_paren ~ (pattern ~ (comma ~ pattern)* ~ comma?)? ~ close_paren } + +pattern_variable = { name } + // ----------------------------------------------------------------------------- // Names @@ -231,6 +249,10 @@ type_keyword = ${ (WHITESPACE | LINE_COMMENT)* ~ TYPE_KEYWORD ~ HORIZONTAL_WHITE foreign_keyword = ${ (WHITESPACE | LINE_COMMENT)* ~ FOREIGN_KEYWORD ~ HORIZONTAL_WHITESPACE? ~ LINE_COMMENT? } +match_keyword = ${ (WHITESPACE | LINE_COMMENT)* ~ MATCH_KEYWORD ~ HORIZONTAL_WHITESPACE? ~ LINE_COMMENT? } + +with_keyword = ${ (WHITESPACE | LINE_COMMENT)* ~ WITH_KEYWORD ~ HORIZONTAL_WHITESPACE? ~ LINE_COMMENT? } + dot = ${ (WHITESPACE | LINE_COMMENT)* ~ DOT ~ HORIZONTAL_WHITESPACE? ~ LINE_COMMENT? } pipe = ${ (WHITESPACE | LINE_COMMENT)* ~ PIPE ~ HORIZONTAL_WHITESPACE? ~ LINE_COMMENT? } @@ -294,6 +316,10 @@ TYPE_KEYWORD = { "type" } FOREIGN_KEYWORD = { "foreign" } +MATCH_KEYWORD = { "match" } + +WITH_KEYWORD = { "with" } + DOT = { "." } PIPE = { "|" } diff --git a/crates/ditto-cst/src/parser/token.rs b/crates/ditto-cst/src/parser/token.rs index 744899fe2..9b15a12df 100644 --- a/crates/ditto-cst/src/parser/token.rs +++ b/crates/ditto-cst/src/parser/token.rs @@ -43,6 +43,8 @@ impl_from_pair!(ElseKeyword, rule = Rule::else_keyword); impl_from_pair!(TypeKeyword, rule = Rule::type_keyword); impl_from_pair!(ForeignKeyword, rule = Rule::foreign_keyword); impl_from_pair!(Pipe, rule = Rule::pipe); +impl_from_pair!(MatchKeyword, rule = Rule::match_keyword); +impl_from_pair!(WithKeyword, rule = Rule::with_keyword); impl StringToken { pub(super) fn from_pairs(pairs: &mut Pairs) -> Self { diff --git a/crates/ditto-cst/src/token.rs b/crates/ditto-cst/src/token.rs index 891ffa819..5a7c811f2 100644 --- a/crates/ditto-cst/src/token.rs +++ b/crates/ditto-cst/src/token.rs @@ -179,3 +179,11 @@ pub struct TypeKeyword(pub EmptyToken); /// `foreign` #[derive(Debug, Clone)] pub struct ForeignKeyword(pub EmptyToken); + +/// `match` +#[derive(Debug, Clone)] +pub struct MatchKeyword(pub EmptyToken); + +/// `with` +#[derive(Debug, Clone)] +pub struct WithKeyword(pub EmptyToken); diff --git a/crates/ditto-fmt/golden-tests/match_expressions.ditto b/crates/ditto-fmt/golden-tests/match_expressions.ditto new file mode 100644 index 000000000..e3a00f13f --- /dev/null +++ b/crates/ditto-fmt/golden-tests/match_expressions.ditto @@ -0,0 +1,49 @@ +module If.Then.Else exports (..); + + +octopus = + match arm with + | Arm1 -> "octopus" + | Arm2 -> -- comment + "octopus" + | Arm3 -> + -- comment + "octopus" + | Arm4 -> "octopus" -- comment + | -- pls don't do this + Arm5 -> "octopus" + | Arm6 -- or this + -> "octopus" + -- + -- + -- + | Arm.Arm7 -> if true then "octopus" else "octopus" + | Arm.Arm8 -> + if true then + -- still octopus + "octopus" + else + "octopus"; + +-- it's a classic +map_maybe = (fn, maybe) -> + -- it really is + match maybe with + | Just(a) -> fn(a) + | Nothing -> Nothing; + +all_the_args = () -> + match not_sure with + | Foo(a, b, c, d) -> unit + | Bar(Foo(a, b), b, Baz.Barrr, d) -> unit + | Foo( + -- comment + a, + B.B, + -- comment + C.C(a, b, c), + D.D( + -- comment + d, + ), + ) -> unit; diff --git a/crates/ditto-fmt/src/expression.rs b/crates/ditto-fmt/src/expression.rs index f642715fa..4ecc5541a 100644 --- a/crates/ditto-fmt/src/expression.rs +++ b/crates/ditto-fmt/src/expression.rs @@ -3,13 +3,14 @@ use super::{ helpers::{group, space}, name::{gen_name, gen_qualified_name, gen_qualified_proper_name}, r#type::gen_type, - syntax::{gen_brackets_list, gen_parens, gen_parens_list}, + syntax::{gen_brackets_list, gen_parens, gen_parens_list, gen_parens_list1}, token::{ - gen_colon, gen_else_keyword, gen_false_keyword, gen_if_keyword, gen_right_arrow, - gen_string_token, gen_then_keyword, gen_true_keyword, gen_unit_keyword, + gen_colon, gen_else_keyword, gen_false_keyword, gen_if_keyword, gen_match_keyword, + gen_pipe, gen_right_arrow, gen_string_token, gen_then_keyword, gen_true_keyword, + gen_unit_keyword, gen_with_keyword, }, }; -use ditto_cst::{Expression, StringToken, TypeAnnotation}; +use ditto_cst::{Expression, MatchArm, Pattern, StringToken, TypeAnnotation}; use dprint_core::formatting::{ condition_helpers, conditions, ir_helpers, ConditionResolver, ConditionResolverContext, Info, PrintItems, Signal, @@ -156,6 +157,66 @@ pub fn gen_expression(expr: Expression) -> PrintItems { })); items } + Expression::Match { + match_keyword, + box expression, + with_keyword, + head_arm, + tail_arms, + } => { + let mut items = PrintItems::new(); + // REVIEW: do we want to support an inline format for single-arm matches? + // + // e.g. `match x with | foo -> bar` + // + // If so, we should probably make that leading `|` optional in the parser + // like we do for type declarations. + items.extend(gen_match_keyword(match_keyword)); + items.extend(space()); + items.extend(gen_expression(expression)); + items.extend(space()); + items.extend(gen_with_keyword(with_keyword)); + items.extend(gen_match_arm(head_arm)); + for match_arm in tail_arms { + items.extend(gen_match_arm(match_arm)); + } + items + } + } +} + +fn gen_match_arm(match_arm: MatchArm) -> PrintItems { + let mut items = PrintItems::new(); + items.push_signal(Signal::ExpectNewLine); + items.extend(gen_pipe(match_arm.pipe)); + items.extend(space()); + items.extend(gen_pattern(match_arm.pattern)); + items.extend(space()); + let right_arrow_has_trailing_comment = match_arm.right_arrow.0.has_trailing_comment(); + items.extend(gen_right_arrow(match_arm.right_arrow)); + items.extend(gen_body_expression( + *match_arm.expression, + right_arrow_has_trailing_comment, + )); + items +} + +fn gen_pattern(pattern: Pattern) -> PrintItems { + match pattern { + Pattern::Variable { name } => gen_name(name), + Pattern::NullaryConstructor { constructor } => gen_qualified_proper_name(constructor), + Pattern::Constructor { + constructor, + arguments, + } => { + let mut items = gen_qualified_proper_name(constructor); + items.extend(gen_parens_list1( + arguments, + |box pattern| gen_pattern(pattern), + false, + )); + items + } } } @@ -168,7 +229,8 @@ pub fn gen_body_expression(expr: Expression, force_use_new_lines: bool) -> Print let end_info = Info::new("end"); let has_leading_comments = expr.has_leading_comments(); - let deserves_new_line_if_multi_lines = matches!(expr, Expression::If { .. }); + let deserves_new_line_if_multi_lines = + matches!(expr, Expression::If { .. } | Expression::Match { .. }); let expression_should_be_on_new_line: ConditionResolver = Rc::new(move |ctx: &mut ConditionResolverContext| -> Option { @@ -350,4 +412,15 @@ mod tests { 20 ); } + + #[test] + fn it_formats_matches() { + assert_fmt!("match foo with\n| var -> 5"); + assert_fmt!("-- comment\nmatch foo with\n| var -> 5"); + assert_fmt!("match foo with\n-- comment\n| var -> 5"); + assert_fmt!("match foo with\n| a -> 5\n| b -> 5\n| c -> 5"); + assert_fmt!("match foo with\n| Foo.Bar -> -- comment\n\t5"); + assert_fmt!("match Foo with\n| Foo(a, b, c) -> a"); + assert_fmt!("match Foo with\n| Foo(\n\t--comment\n\ta,\n\tb,\n\tc,\n) -> a"); + } } diff --git a/crates/ditto-fmt/src/has_comments.rs b/crates/ditto-fmt/src/has_comments.rs index a9096c669..6323b3b9b 100644 --- a/crates/ditto-fmt/src/has_comments.rs +++ b/crates/ditto-fmt/src/has_comments.rs @@ -57,6 +57,19 @@ impl HasComments for Expression { function, arguments, } => function.has_comments() || arguments.has_comments(), + Self::Match { + match_keyword, + expression, + with_keyword, + head_arm, + tail_arms, + } => { + match_keyword.0.has_comments() + || expression.has_comments() + || with_keyword.0.has_comments() + || head_arm.has_comments() + || tail_arms.iter().any(|arm| arm.has_comments()) + } } } @@ -75,6 +88,45 @@ impl HasComments for Expression { Self::If { if_keyword, .. } => if_keyword.0.has_leading_comments(), Self::Function { box parameters, .. } => parameters.open_paren.0.has_leading_comments(), Self::Call { function, .. } => function.has_leading_comments(), + Self::Match { match_keyword, .. } => match_keyword.0.has_leading_comments(), + } + } +} + +impl HasComments for MatchArm { + fn has_comments(&self) -> bool { + let Self { + pipe, + pattern, + right_arrow, + expression, + } = self; + pipe.0.has_comments() + || pattern.has_comments() + || right_arrow.0.has_comments() + || expression.has_comments() + } + fn has_leading_comments(&self) -> bool { + self.pipe.0.has_leading_comments() + } +} + +impl HasComments for Pattern { + fn has_comments(&self) -> bool { + match self { + Self::NullaryConstructor { constructor } => constructor.has_comments(), + Self::Constructor { + constructor, + arguments, + } => constructor.has_comments() || arguments.has_comments(), + Self::Variable { name } => name.has_comments(), + } + } + fn has_leading_comments(&self) -> bool { + match self { + Self::NullaryConstructor { constructor } => constructor.has_leading_comments(), + Self::Constructor { constructor, .. } => constructor.has_leading_comments(), + Self::Variable { name } => name.has_leading_comments(), } } } diff --git a/crates/ditto-fmt/src/token.rs b/crates/ditto-fmt/src/token.rs index 4603666de..36ce7573c 100644 --- a/crates/ditto-fmt/src/token.rs +++ b/crates/ditto-fmt/src/token.rs @@ -32,6 +32,8 @@ gen_empty_token_like!(gen_unit_keyword, cst::UnitKeyword, "unit"); gen_empty_token_like!(gen_if_keyword, cst::IfKeyword, "if"); gen_empty_token_like!(gen_then_keyword, cst::ThenKeyword, "then"); gen_empty_token_like!(gen_else_keyword, cst::ElseKeyword, "else"); +gen_empty_token_like!(gen_match_keyword, cst::MatchKeyword, "match"); +gen_empty_token_like!(gen_with_keyword, cst::WithKeyword, "with"); gen_empty_token_like!(gen_exports_keyword, cst::ExportsKeyword, "exports"); gen_empty_token_like!(gen_as_keyword, cst::AsKeyword, "as"); gen_empty_token_like!(gen_type_keyword, cst::TypeKeyword, "type"); diff --git a/crates/ditto-lsp/src/semantic_tokens.rs b/crates/ditto-lsp/src/semantic_tokens.rs index 90214ffb1..20eeae7a3 100644 --- a/crates/ditto-lsp/src/semantic_tokens.rs +++ b/crates/ditto-lsp/src/semantic_tokens.rs @@ -111,6 +111,8 @@ impl TokensBuilder { "as" "type" "foreign" + "match" + "with" ] @keyword ; 2, 3, 4