diff --git a/CHANGELOG.md b/CHANGELOG.md index 5944025560..5edced5477 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,9 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### New features +* Templates now support the `>`, `>=`, `<`, and `<=` relational operators for + `Integer` types. + ### Fixed bugs * The `$NO_COLOR` environment variable must now be non-empty to be respected. diff --git a/cli/src/template.pest b/cli/src/template.pest index 3061628fa3..bef4093e25 100644 --- a/cli/src/template.pest +++ b/cli/src/template.pest @@ -42,10 +42,23 @@ logical_or_op = { "||" } logical_and_op = { "&&" } logical_eq_op = { "==" } logical_ne_op = { "!=" } +gte_op = { ">=" } +gt_op = { ">" } +lte_op = { "<=" } +lt_op = { "<" } logical_not_op = { "!" } negate_op = { "-" } prefix_ops = _{ logical_not_op | negate_op } -infix_ops = _{ logical_or_op | logical_and_op | logical_eq_op | logical_ne_op } +infix_ops = _{ + logical_or_op + | logical_and_op + | logical_eq_op + | logical_ne_op + | gte_op + | gt_op + | lte_op + | lt_op +} function = { identifier ~ "(" ~ whitespace* ~ function_arguments ~ whitespace* ~ ")" } keyword_argument = { identifier ~ whitespace* ~ "=" ~ whitespace* ~ template } diff --git a/cli/src/template_builder.rs b/cli/src/template_builder.rs index 1178e9ac67..5de6068f71 100644 --- a/cli/src/template_builder.rs +++ b/cli/src/template_builder.rs @@ -705,6 +705,36 @@ fn build_binary_operation<'a, L: TemplateLanguage<'a> + ?Sized>( _ => unreachable!(), } } + BinaryOp::Gt | BinaryOp::Lte => { + let lhs = build_expression(language, diagnostics, build_ctx, lhs_node)?; + let rhs = build_expression(language, diagnostics, build_ctx, rhs_node)?; + let lty = lhs.type_name(); + let rty = rhs.type_name(); + let out = lhs.try_into_gt(rhs).ok_or_else(|| { + let message = format!(r#"Cannot compare expressions of type "{lty}" and "{rty}""#); + TemplateParseError::expression(message, span) + })?; + match op { + BinaryOp::Gt => Ok(L::wrap_boolean(out)), + BinaryOp::Lte => Ok(L::wrap_boolean(out.map(|eq| !eq))), + _ => unreachable!(), + } + } + BinaryOp::Lt | BinaryOp::Gte => { + let lhs = build_expression(language, diagnostics, build_ctx, lhs_node)?; + let rhs = build_expression(language, diagnostics, build_ctx, rhs_node)?; + let lty = lhs.type_name(); + let rty = rhs.type_name(); + let out = lhs.try_into_lt(rhs).ok_or_else(|| { + let message = format!(r#"Cannot compare expressions of type "{lty}" and "{rty}""#); + TemplateParseError::expression(message, span) + })?; + match op { + BinaryOp::Lt => Ok(L::wrap_boolean(out)), + BinaryOp::Gte => Ok(L::wrap_boolean(out.map(|eq| !eq))), + _ => unreachable!(), + } + } } } @@ -1784,14 +1814,14 @@ mod tests { env.add_keyword("description", || L::wrap_string(Literal("".to_owned()))); env.add_keyword("empty", || L::wrap_boolean(Literal(true))); - insta::assert_snapshot!(env.parse_err(r#"description ()"#), @r" + insta::assert_snapshot!(env.parse_err(r#"description ()"#), @r#" --> 1:13 | 1 | description () | ^--- | - = expected , `++`, `||`, `&&`, `==`, or `!=` - "); + = expected , `++`, `||`, `&&`, `==`, `!=`, `>=`, `>`, `<=`, or `<` + "#); insta::assert_snapshot!(env.parse_err(r#"foo"#), @r###" --> 1:1 @@ -1891,6 +1921,14 @@ mod tests { | = Cannot compare expressions of type "String" and "Template" "#); + insta::assert_snapshot!(env.parse_err(r#"'a' > 1"#), @r#" + --> 1:1 + | + 1 | 'a' > 1 + | ^-----^ + | + = Cannot compare expressions of type "String" and "Integer" + "#); insta::assert_snapshot!(env.parse_err(r#"description.first_line().foo()"#), @r###" --> 1:26 @@ -2101,6 +2139,20 @@ mod tests { @""); } + #[test] + fn test_relational_operation() { + let env = TestTemplateEnv::new(); + + insta::assert_snapshot!(env.render_ok(r#"1 >= 1"#), @"true"); + insta::assert_snapshot!(env.render_ok(r#"0 >= 1"#), @"false"); + insta::assert_snapshot!(env.render_ok(r#"2 > 1"#), @"true"); + insta::assert_snapshot!(env.render_ok(r#"1 > 1"#), @"false"); + insta::assert_snapshot!(env.render_ok(r#"1 <= 1"#), @"true"); + insta::assert_snapshot!(env.render_ok(r#"2 <= 1"#), @"false"); + insta::assert_snapshot!(env.render_ok(r#"0 < 1"#), @"true"); + insta::assert_snapshot!(env.render_ok(r#"1 < 1"#), @"false"); + } + #[test] fn test_logical_operation() { let mut env = TestTemplateEnv::new(); diff --git a/cli/src/template_parser.rs b/cli/src/template_parser.rs index 406e12bf2a..2a706ce8a3 100644 --- a/cli/src/template_parser.rs +++ b/cli/src/template_parser.rs @@ -76,6 +76,10 @@ impl Rule { Rule::logical_and_op => Some("&&"), Rule::logical_eq_op => Some("=="), Rule::logical_ne_op => Some("!="), + Rule::gte_op => Some(">="), + Rule::gt_op => Some(">"), + Rule::lte_op => Some("<="), + Rule::lt_op => Some("<"), Rule::logical_not_op => Some("!"), Rule::negate_op => Some("-"), Rule::prefix_ops => None, @@ -380,6 +384,14 @@ pub enum BinaryOp { LogicalEq, /// `!=` LogicalNe, + /// `>=` + Gte, + /// `>` + Gt, + /// `<=` + Lte, + /// `<` + Lt, } pub type ExpressionNode<'i> = dsl_util::ExpressionNode<'i, ExpressionKind<'i>>; @@ -512,6 +524,10 @@ fn parse_expression_node(pair: Pair) -> TemplateParseResult) -> TemplateParseResult BinaryOp::LogicalAnd, Rule::logical_eq_op => BinaryOp::LogicalEq, Rule::logical_ne_op => BinaryOp::LogicalNe, + Rule::gte_op => BinaryOp::Gte, + Rule::gt_op => BinaryOp::Gt, + Rule::lte_op => BinaryOp::Lte, + Rule::lt_op => BinaryOp::Lt, r => panic!("unexpected infix operator rule {r:?}"), }; let lhs = Box::new(lhs?); @@ -861,8 +881,14 @@ mod tests { parse_normalized("(!(x.f())) || (!(g()))"), ); assert_eq!( - parse_normalized("!x.f() == !x.f() || !g() != !g()"), - parse_normalized("((!(x.f())) == (!(x.f()))) || ((!(g())) != (!(g())))"), + parse_normalized("!x.f() <= !x.f()"), + parse_normalized("((!(x.f())) <= (!(x.f())))"), + ); + assert_eq!( + parse_normalized("!x.f() < !x.f() == !x.f() >= !x.f() || !g() != !g()"), + parse_normalized( + "((!(x.f()) < (!(x.f()))) == ((!(x.f())) >= (!(x.f())))) || ((!(g())) != (!(g())))" + ), ); assert_eq!( parse_normalized("x.f() || y == y || z"), diff --git a/cli/tests/test_templater.rs b/cli/tests/test_templater.rs index 0bda44c379..7c62a57157 100644 --- a/cli/tests/test_templater.rs +++ b/cli/tests/test_templater.rs @@ -25,15 +25,15 @@ fn test_templater_parse_error() { let repo_path = test_env.env_root().join("repo"); let render_err = |template| test_env.jj_cmd_failure(&repo_path, &["log", "-T", template]); - insta::assert_snapshot!(render_err(r#"description ()"#), @r" + insta::assert_snapshot!(render_err(r#"description ()"#), @r#" Error: Failed to parse template: Syntax error Caused by: --> 1:13 | 1 | description () | ^--- | - = expected , `++`, `||`, `&&`, `==`, or `!=` - "); + = expected , `++`, `||`, `&&`, `==`, `!=`, `>=`, `>`, `<=`, or `<` + "#); // Typo test_env.add_config( diff --git a/docs/templates.md b/docs/templates.md index 58484135fd..8c927c6839 100644 --- a/docs/templates.md +++ b/docs/templates.md @@ -31,6 +31,8 @@ The following operators are supported. * `x.f()`: Method call. * `-x`: Negate integer value. * `!x`: Logical not. +* `x >= y`, `x > y`, `x <= y`, `x < y`: Greater than or equal/greater than/ + lesser than or equal/lesser than. Operands must be `Integer`s. * `x == y`, `x != y`: Logical equal/not equal. Operands must be either `Boolean`, `Integer`, or `String`. * `x && y`: Logical and, short-circuiting.