diff --git a/crates/ditto-checker/golden-tests/type-errors/argument_length_mismatch_3.ditto b/crates/ditto-checker/golden-tests/type-errors/argument_length_mismatch_3.ditto new file mode 100644 index 000000000..122e70c89 --- /dev/null +++ b/crates/ditto-checker/golden-tests/type-errors/argument_length_mismatch_3.ditto @@ -0,0 +1,3 @@ +module Test exports (..); + +bad_pipe = 5 |> (() -> 10); diff --git a/crates/ditto-checker/golden-tests/type-errors/argument_length_mismatch_3.error b/crates/ditto-checker/golden-tests/type-errors/argument_length_mismatch_3.error new file mode 100644 index 000000000..5674ea061 --- /dev/null +++ b/crates/ditto-checker/golden-tests/type-errors/argument_length_mismatch_3.error @@ -0,0 +1,9 @@ + + × wrong number of arguments + ╭─[golden:1:1] + 1 │ module Test exports (..); + 2 │ + 3 │ bad_pipe = 5 |> (() -> 10); + · ────┬─── + · ╰── this expects no arguments + ╰──── diff --git a/crates/ditto-checker/golden-tests/type-errors/not_a_function_1.ditto b/crates/ditto-checker/golden-tests/type-errors/not_a_function_1.ditto new file mode 100644 index 000000000..08768d768 --- /dev/null +++ b/crates/ditto-checker/golden-tests/type-errors/not_a_function_1.ditto @@ -0,0 +1,3 @@ +module Test exports (..); + +huh = 5 |> 5; diff --git a/crates/ditto-checker/golden-tests/type-errors/not_a_function_1.error b/crates/ditto-checker/golden-tests/type-errors/not_a_function_1.error new file mode 100644 index 000000000..bd9f43eea --- /dev/null +++ b/crates/ditto-checker/golden-tests/type-errors/not_a_function_1.error @@ -0,0 +1,10 @@ + + × expression isn't callable + ╭─[golden:1:1] + 1 │ module Test exports (..); + 2 │ + 3 │ huh = 5 |> 5; + · ┬ + · ╰── can't call this + ╰──── + help: expression has type: Int diff --git a/crates/ditto-checker/golden-tests/type-errors/unification_error_4.ditto b/crates/ditto-checker/golden-tests/type-errors/unification_error_4.ditto new file mode 100644 index 000000000..e95b1966f --- /dev/null +++ b/crates/ditto-checker/golden-tests/type-errors/unification_error_4.ditto @@ -0,0 +1,5 @@ +module Test exports (..); + +identity_int = (n: Int): Int -> n; + +huh = "nope" |> identity_int; diff --git a/crates/ditto-checker/golden-tests/type-errors/unification_error_4.error b/crates/ditto-checker/golden-tests/type-errors/unification_error_4.error new file mode 100644 index 000000000..9289e5ae1 --- /dev/null +++ b/crates/ditto-checker/golden-tests/type-errors/unification_error_4.error @@ -0,0 +1,12 @@ + + × types don't unify + ╭─[golden:2:1] + 2 │ + 3 │ identity_int = (n: Int): Int -> n; + 4 │ + 5 │ huh = "nope" |> identity_int; + · ───┬── + · ╰── here + ╰──── + help: expected Int + got String diff --git a/crates/ditto-checker/src/module/value_declarations/mod.rs b/crates/ditto-checker/src/module/value_declarations/mod.rs index c8ced46fb..b496c978b 100644 --- a/crates/ditto-checker/src/module/value_declarations/mod.rs +++ b/crates/ditto-checker/src/module/value_declarations/mod.rs @@ -480,6 +480,14 @@ fn toposort_value_declarations( Expression::Parens(parens) => { get_connected_nodes_rec(&parens.value, nodes, accum); } + Expression::BinOp { + box lhs, + operator: _, + box rhs, + } => { + get_connected_nodes_rec(lhs, nodes, accum); + get_connected_nodes_rec(rhs, nodes, accum); + } // noop Expression::Constructor(_qualified_proper_name) => {} Expression::String(_) => {} diff --git a/crates/ditto-checker/src/typechecker/pre_ast.rs b/crates/ditto-checker/src/typechecker/pre_ast.rs index 2d6418f54..9a84a7084 100644 --- a/crates/ditto-checker/src/typechecker/pre_ast.rs +++ b/crates/ditto-checker/src/typechecker/pre_ast.rs @@ -302,6 +302,39 @@ fn convert_cst( body: Box::new(body), }) } + cst::Expression::BinOp { + box lhs, + // Desugar! + operator: cst::BinOp::RightPizza(_), + box rhs, + } => { + let lhs = convert_cst(env, state, lhs)?; + let rhs = convert_cst(env, state, rhs)?; + match rhs { + Expression::Call { + span, + function, + arguments: original_arguments, + } => { + // Push the lhs as the first argument to the rhs + let mut arguments = vec![Argument::Expression(lhs)]; + arguments.extend(original_arguments); + Ok(Expression::Call { + span, + function, + arguments, + }) + } + function => { + let arguments = vec![Argument::Expression(lhs)]; + Ok(Expression::Call { + span, + function: Box::new(function), + arguments, + }) + } + } + } } } diff --git a/crates/ditto-checker/src/typechecker/tests/mod.rs b/crates/ditto-checker/src/typechecker/tests/mod.rs index b67e18d18..997cb4d21 100644 --- a/crates/ditto-checker/src/typechecker/tests/mod.rs +++ b/crates/ditto-checker/src/typechecker/tests/mod.rs @@ -8,5 +8,6 @@ mod function; mod int; pub(self) mod macros; mod r#match; +mod right_pipe; mod string; mod unit; diff --git a/crates/ditto-checker/src/typechecker/tests/right_pipe.rs b/crates/ditto-checker/src/typechecker/tests/right_pipe.rs new file mode 100644 index 000000000..61c111b66 --- /dev/null +++ b/crates/ditto-checker/src/typechecker/tests/right_pipe.rs @@ -0,0 +1,17 @@ +use super::macros::*; +use crate::TypeError::*; + +#[test] +fn it_typechecks_as_expected() { + assert_type!("(f) -> 5 |> f", "((Int) -> $1) -> $1"); + assert_type!("(f) -> 5 |> f()", "((Int) -> $1) -> $1"); + assert_type!("(f, g) -> 5 |> f |> g", "((Int) -> $2, ($2) -> $3) -> $3"); + assert_type!("5 |> ((n) -> n)", "Int"); + assert_type!("5 |> ((n) -> n)()", "Int"); +} + +#[test] +fn it_errors_as_expected() { + assert_type_error!("5 |> 5", NotAFunction { .. }); + assert_type_error!("5 |> (() -> 5)", ArgumentLengthMismatch { .. }); +} diff --git a/crates/ditto-codegen-js/golden-tests/javascript/pipe_expressions.ditto b/crates/ditto-codegen-js/golden-tests/javascript/pipe_expressions.ditto new file mode 100644 index 000000000..b88f0efae --- /dev/null +++ b/crates/ditto-codegen-js/golden-tests/javascript/pipe_expressions.ditto @@ -0,0 +1,18 @@ +module Test exports (..); + +identity = (a) -> a; + +with_parens = 5 |> identity; +without_parens = 5 |> identity(); + +inline_function = 5 |> ((n) -> n); + +tagged_identity = (a, tag) -> a; + +-- these two should look the same in the generated code! +want = tagged_identity(tagged_identity(tagged_identity(5, "1"), "2"), "3"); +got_ = + 5 + |> tagged_identity("1") + |> tagged_identity("2") + |> tagged_identity("3"); diff --git a/crates/ditto-codegen-js/golden-tests/javascript/pipe_expressions.js b/crates/ditto-codegen-js/golden-tests/javascript/pipe_expressions.js new file mode 100644 index 000000000..a065e36ee --- /dev/null +++ b/crates/ditto-codegen-js/golden-tests/javascript/pipe_expressions.js @@ -0,0 +1,26 @@ +function tagged_identity(a, tag) { + return a; +} +const got_ = tagged_identity( + tagged_identity(tagged_identity(5, "1"), "2"), + "3", +); +const want = tagged_identity( + tagged_identity(tagged_identity(5, "1"), "2"), + "3", +); +const inline_function = (n => n)(5); +function identity(a) { + return a; +} +const without_parens = identity(5); +const with_parens = identity(5); +export { + got_, + identity, + inline_function, + tagged_identity, + want, + with_parens, + without_parens, +}; diff --git a/crates/ditto-cst/src/expression.rs b/crates/ditto-cst/src/expression.rs index 3b576909a..34e349aa7 100644 --- a/crates/ditto-cst/src/expression.rs +++ b/crates/ditto-cst/src/expression.rs @@ -1,8 +1,8 @@ use crate::{ BracketsList, CloseBrace, Colon, DoKeyword, ElseKeyword, FalseKeyword, IfKeyword, LeftArrow, MatchKeyword, Name, OpenBrace, Parens, ParensList, ParensList1, Pipe, QualifiedName, - QualifiedProperName, ReturnKeyword, RightArrow, Semicolon, StringToken, ThenKeyword, - TrueKeyword, Type, UnitKeyword, WithKeyword, + QualifiedProperName, ReturnKeyword, RightArrow, RightPizzaOperator, Semicolon, StringToken, + ThenKeyword, TrueKeyword, Type, UnitKeyword, WithKeyword, }; /// A value expression. @@ -125,6 +125,22 @@ pub enum Expression { Float(StringToken), /// `[this, is, an, array]` Array(BracketsList>), + /// Binary operator expression.s + BinOp { + /// The left-hand side of the operator. + lhs: Box, + /// The binary operator. + operator: BinOp, + /// The right-hand side of the operator. + rhs: Box, + }, +} + +/// A binary operator. +#[derive(Debug, Clone)] +pub enum BinOp { + /// `|>` + RightPizza(RightPizzaOperator), } /// A chain of Effect statements. diff --git a/crates/ditto-cst/src/get_span.rs b/crates/ditto-cst/src/get_span.rs index 0991b5507..1393737fc 100644 --- a/crates/ditto-cst/src/get_span.rs +++ b/crates/ditto-cst/src/get_span.rs @@ -110,6 +110,7 @@ impl Expression { Self::True(true_keyword) => true_keyword.0.get_span(), Self::False(false_keyword) => false_keyword.0.get_span(), Self::Unit(unit_keyword) => unit_keyword.0.get_span(), + Self::BinOp { lhs, rhs, .. } => lhs.get_span().merge(&rhs.get_span()), } } } diff --git a/crates/ditto-cst/src/parser/expression.rs b/crates/ditto-cst/src/parser/expression.rs index 27bd14ae4..a16234d5e 100644 --- a/crates/ditto-cst/src/parser/expression.rs +++ b/crates/ditto-cst/src/parser/expression.rs @@ -1,9 +1,10 @@ use super::{parse_rule, Result, Rule}; use crate::{ - BracketsList, CloseBrace, Colon, DoKeyword, Effect, ElseKeyword, Expression, FalseKeyword, - IfKeyword, LeftArrow, MatchArm, MatchKeyword, Name, OpenBrace, Parens, ParensList, ParensList1, - Pattern, Pipe, QualifiedName, QualifiedProperName, ReturnKeyword, RightArrow, Semicolon, - StringToken, ThenKeyword, TrueKeyword, Type, TypeAnnotation, UnitKeyword, WithKeyword, + BinOp, BracketsList, CloseBrace, Colon, DoKeyword, Effect, ElseKeyword, Expression, + FalseKeyword, IfKeyword, LeftArrow, MatchArm, MatchKeyword, Name, OpenBrace, Parens, + ParensList, ParensList1, Pattern, Pipe, QualifiedName, QualifiedProperName, ReturnKeyword, + RightArrow, RightPizzaOperator, Semicolon, StringToken, ThenKeyword, TrueKeyword, Type, + TypeAnnotation, UnitKeyword, WithKeyword, }; use pest::iterators::Pair; @@ -151,6 +152,24 @@ impl Expression { close_brace, } } + Rule::expression_right_pipe => { + let mut inner = pair.into_inner(); + let lhs = Box::new(Expression::from_pair(inner.next().unwrap())); + let operator = + BinOp::RightPizza(RightPizzaOperator::from_pair(inner.next().unwrap())); + let rhs = Box::new(Expression::from_pair(inner.next().unwrap())); + let mut expression = Self::BinOp { lhs, operator, rhs }; + while let Some(pair) = inner.next() { + let operator = BinOp::RightPizza(RightPizzaOperator::from_pair(pair)); + let rhs = Box::new(Expression::from_pair(inner.next().unwrap())); + expression = Self::BinOp { + lhs: Box::new(expression), + operator, + rhs, + } + } + expression + } other => unreachable!("{:#?} {:#?}", other, pair.into_inner()), } } @@ -263,7 +282,7 @@ impl TypeAnnotation { #[cfg(test)] mod tests { use super::test_macros::*; - use crate::{Brackets, CommaSep1, Expression, Parens, StringToken}; + use crate::{BinOp, Brackets, CommaSep1, Expression, Parens, StringToken}; #[test] fn it_parses_constructors() { @@ -627,6 +646,51 @@ mod tests { } ); } + + #[test] + fn it_parses_pipes() { + assert_parses!( + "x |> y", + Expression::BinOp { + operator: BinOp::RightPizza(_), + .. + } + ); + + assert_parses!( + "x() |> y()", + Expression::BinOp { + operator: BinOp::RightPizza(_), + .. + } + ); + // Left associative + assert_parses!( + "x |> y |> z", + Expression::BinOp { + operator: BinOp::RightPizza(_), + lhs: box Expression::BinOp { + operator: BinOp::RightPizza(_), + .. + }, + .. + } + ); + assert_parses!( + "x |> (y |> z)", + Expression::BinOp { + operator: BinOp::RightPizza(_), + rhs: box Expression::Parens(Parens { + value: box Expression::BinOp { + operator: BinOp::RightPizza(_), + .. + }, + .. + }), + .. + } + ); + } } #[cfg(test)] diff --git a/crates/ditto-cst/src/parser/grammar.pest b/crates/ditto-cst/src/parser/grammar.pest index 3f0c26b54..6584ad4b3 100644 --- a/crates/ditto-cst/src/parser/grammar.pest +++ b/crates/ditto-cst/src/parser/grammar.pest @@ -129,20 +129,29 @@ return_type_annotation = { colon ~ type1 } // used for function expressions only // Expressions expression = _ - { expression_call - | expression_function + { expression_function | expression_match | expression_effect - | expression1 + | expression_if + | expression_2 + } + +expression_2 = _ + { expression_right_pipe + | expression_1 } -expression1 = _ +expression_1 = _ + { expression_call + | expression_0 + } + +expression_0 = _ { expression_parens | expression_constructor | expression_true | expression_false | expression_unit - | expression_if // It's important that keyword expressions come before variable | expression_variable | expression_array @@ -151,11 +160,14 @@ expression1 = _ | expression_integer } +// Left-associative (so needs some left recursion hacking) +expression_right_pipe = { expression_1 ~ right_pizza ~ expression_1 ~ (right_pizza ~ expression_1)* } + expression_parens = { open_paren ~ expression ~ close_paren } // No left recursion yet :( // https://github.com/pest-parser/pest/pull/533 -expression_call = { expression1 ~ expression_call_arguments+ } +expression_call = { expression_0 ~ expression_call_arguments+ } expression_call_arguments = { open_paren ~ (expression ~ (comma ~ expression)* ~ comma?)? ~ close_paren } @@ -278,6 +290,8 @@ dot = ${ (WHITESPACE | LINE_COMMENT)* ~ DOT ~ HORIZONTAL_WHITESPACE? ~ LINE_COMM pipe = ${ (WHITESPACE | LINE_COMMENT)* ~ PIPE ~ HORIZONTAL_WHITESPACE? ~ LINE_COMMENT? } +right_pizza = ${ (WHITESPACE | LINE_COMMENT)* ~ RIGHT_PIZZA ~ HORIZONTAL_WHITESPACE? ~ LINE_COMMENT? } + double_dot = ${ (WHITESPACE | LINE_COMMENT)* ~ DOUBLE_DOT ~ HORIZONTAL_WHITESPACE? ~ LINE_COMMENT? } comma = ${ (WHITESPACE | LINE_COMMENT)* ~ COMMA ~ HORIZONTAL_WHITESPACE? ~ LINE_COMMENT? } @@ -357,6 +371,8 @@ DOT = { "." } PIPE = { "|" } +RIGHT_PIZZA = { "|>" } + DOUBLE_DOT = { ".." } COMMA = { "," } diff --git a/crates/ditto-cst/src/parser/token.rs b/crates/ditto-cst/src/parser/token.rs index 56a8c9f4c..da25e9904 100644 --- a/crates/ditto-cst/src/parser/token.rs +++ b/crates/ditto-cst/src/parser/token.rs @@ -5,7 +5,7 @@ use crate::{ AsKeyword, CloseBrace, CloseBracket, CloseParen, Colon, Comma, Comment, DoKeyword, DoubleDot, EmptyToken, Equals, ExportsKeyword, FalseKeyword, ForeignKeyword, ImportKeyword, LeftArrow, LetKeyword, ModuleKeyword, OpenBrace, OpenBracket, OpenParen, Pipe, ReturnKeyword, RightArrow, - Span, StringToken, TrueKeyword, TypeKeyword, UnitKeyword, + RightPizzaOperator, Span, StringToken, TrueKeyword, TypeKeyword, UnitKeyword, }; use pest::iterators::{Pair, Pairs}; @@ -52,6 +52,7 @@ impl_from_pair!(WithKeyword, rule = Rule::with_keyword); //impl_from_pair!(LetKeyword, rule = Rule::let_keyword); impl_from_pair!(DoKeyword, rule = Rule::do_keyword); impl_from_pair!(ReturnKeyword, rule = Rule::return_keyword); +impl_from_pair!(RightPizzaOperator, rule = Rule::right_pizza); 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 0efaf75ed..a919ddd92 100644 --- a/crates/ditto-cst/src/token.rs +++ b/crates/ditto-cst/src/token.rs @@ -207,3 +207,7 @@ pub struct DoKeyword(pub EmptyToken); /// `return` #[derive(Debug, Clone)] pub struct ReturnKeyword(pub EmptyToken); + +/// `|>` +#[derive(Debug, Clone)] +pub struct RightPizzaOperator(pub EmptyToken); diff --git a/crates/ditto-fmt/golden-tests/pipe_expressions.ditto b/crates/ditto-fmt/golden-tests/pipe_expressions.ditto new file mode 100644 index 000000000..be97b01f3 --- /dev/null +++ b/crates/ditto-fmt/golden-tests/pipe_expressions.ditto @@ -0,0 +1,20 @@ +module Pipe exports (..); + + +long_pipe = + a + |> Some.function() -- comment + -- comment + |> Some.other_function + |> Some.function_with_arguments(a, b, c) + |> Some.function_with_multiline_arguments( + -- comment + a, + b, + [], + ) + |> ( + a + |> b + ) + |> done; diff --git a/crates/ditto-fmt/src/expression.rs b/crates/ditto-fmt/src/expression.rs index 29e253019..9c0e3fd48 100644 --- a/crates/ditto-fmt/src/expression.rs +++ b/crates/ditto-fmt/src/expression.rs @@ -7,11 +7,11 @@ use super::{ token::{ gen_close_brace, gen_colon, gen_do_keyword, gen_else_keyword, gen_false_keyword, gen_if_keyword, gen_left_arrow, gen_match_keyword, gen_open_brace, gen_pipe, - gen_return_keyword, gen_right_arrow, gen_semicolon, gen_string_token, gen_then_keyword, - gen_true_keyword, gen_unit_keyword, gen_with_keyword, + gen_return_keyword, gen_right_arrow, gen_right_pizza_operator, gen_semicolon, + gen_string_token, gen_then_keyword, gen_true_keyword, gen_unit_keyword, gen_with_keyword, }, }; -use ditto_cst::{Effect, Expression, MatchArm, Pattern, StringToken, TypeAnnotation}; +use ditto_cst::{BinOp, Effect, Expression, MatchArm, Pattern, StringToken, TypeAnnotation}; use dprint_core::formatting::{ condition_helpers, conditions, ir_helpers, ConditionResolver, ConditionResolverContext, Info, PrintItems, Signal, @@ -207,6 +207,19 @@ pub fn gen_expression(expr: Expression) -> PrintItems { } items } + Expression::BinOp { + box lhs, + operator: BinOp::RightPizza(right_pizza_operator), + box rhs, + } => { + let mut items = PrintItems::new(); + items.extend(gen_expression(lhs)); + items.push_signal(Signal::ExpectNewLine); + items.extend(gen_right_pizza_operator(right_pizza_operator)); + items.extend(space()); + items.extend(gen_expression(rhs)); + items + } } } @@ -294,8 +307,15 @@ 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 { .. } | Expression::Match { .. }); + let deserves_new_line_if_multi_lines = matches!( + expr, + Expression::If { .. } + | Expression::Match { .. } + | Expression::BinOp { + operator: BinOp::RightPizza(_), + .. + } + ); let expression_should_be_on_new_line: ConditionResolver = Rc::new(move |ctx: &mut ConditionResolverContext| -> Option { @@ -496,4 +516,12 @@ mod tests { assert_fmt!("do {\n\tx <- some_effect();\n\treturn x\n}"); assert_fmt!("do {\n\tsome_effect();\n\treturn 5\n}"); } + + #[test] + fn it_formats_pipes() { + assert_fmt!("x\n|> y"); + assert_fmt!("-- comment\nx\n|> y"); + assert_fmt!("x\n|> y\n|> z"); + assert_fmt!("(x |> y) |> z", "(\n\tx\n\t|> y\n)\n|> z"); + } } diff --git a/crates/ditto-fmt/src/has_comments.rs b/crates/ditto-fmt/src/has_comments.rs index f278aadcc..4c71a1295 100644 --- a/crates/ditto-fmt/src/has_comments.rs +++ b/crates/ditto-fmt/src/has_comments.rs @@ -81,6 +81,9 @@ impl HasComments for Expression { || effect.has_comments() || close_brace.0.has_comments() } + Self::BinOp { lhs, operator, rhs } => { + lhs.has_comments() || operator.has_comments() || rhs.has_comments() + } } } @@ -101,6 +104,7 @@ impl HasComments for Expression { Self::Call { function, .. } => function.has_leading_comments(), Self::Match { match_keyword, .. } => match_keyword.0.has_leading_comments(), Self::Effect { do_keyword, .. } => do_keyword.0.has_leading_comments(), + Self::BinOp { lhs, .. } => lhs.has_leading_comments(), } } } @@ -178,6 +182,19 @@ impl HasComments for Pattern { } } +impl HasComments for BinOp { + fn has_comments(&self) -> bool { + match self { + Self::RightPizza(token) => token.0.has_comments(), + } + } + fn has_leading_comments(&self) -> bool { + match self { + Self::RightPizza(token) => token.0.has_leading_comments(), + } + } +} + impl HasComments for Type { fn has_comments(&self) -> bool { match self { diff --git a/crates/ditto-fmt/src/token.rs b/crates/ditto-fmt/src/token.rs index f710b6051..2a8428ea1 100644 --- a/crates/ditto-fmt/src/token.rs +++ b/crates/ditto-fmt/src/token.rs @@ -54,6 +54,7 @@ gen_empty_token_like!(gen_left_arrow, cst::LeftArrow, "<-"); gen_empty_token_like!(gen_module_keyword, cst::ModuleKeyword, "module"); gen_empty_token_like!(gen_do_keyword, cst::DoKeyword, "do"); gen_empty_token_like!(gen_return_keyword, cst::ReturnKeyword, "return"); +gen_empty_token_like!(gen_right_pizza_operator, cst::RightPizzaOperator, "|>"); gen_empty_token_like!( gen_close_bracket, cst::CloseBracket,