templater: add parsing rule for lambda expression

A lambda expression will be allowed only in .map() operation. The syntax is
borrowed from Rust closure.

In Mercurial, a map operation is implemented by context substitution. For
example, 'parents % "{node}"' prints parents[i].node for each. There are two
major problems: 1. the top-level context cannot be referred from the inner map
expression. 2. context of different types inserts arbitrarily-named keywords
(e.g. a dict type inserts "{key}" and "{value}", but how we could know.)

These issues should be avoided by using explicitly named parameters.

    parents.map(|parent| parent.commit_id ++ " " ++ commit_id)
                                                    ^^^^^^^^^ global keyword

A downside is that we can't reuse template fragment in map expression. Suppose
we have -T commit_summary, -T 'parents.map(commit_summary)' doesn't work.

    # only usable as a top-level template
    'commit_summary' = 'commit_id.short() ++ " " ++ description.first_line()'

Another problem is that a lambda expression might be confused with an alias
function.

    # .map(f) doesn't work, but .map(g) does
    'f(x)' = 'x'
    'g' = '|x| x'
This commit is contained in:
Yuya Nishihara 2023-03-07 19:14:00 +09:00
parent e2b4d7058d
commit 1c0bde1a2b
4 changed files with 163 additions and 20 deletions

View File

@ -14,8 +14,8 @@
// Example:
// "commit: " ++ short(commit_id) ++ "\n"
// predecessors % ("predecessor: " ++ commit_id)
// parents % (commit_id ++ " is a parent of " ++ super.commit_id)
// predecessors.map(|p| "predecessor: " ++ p.commit_id)
// parents.map(|p| p.commit_id ++ " is a parent of " ++ commit_id)
whitespace = _{ " " | "\t" | "\r" | "\n" | "\x0c" }
@ -36,6 +36,10 @@ function_arguments = {
template ~ (whitespace* ~ "," ~ whitespace* ~ template)* ~ (whitespace* ~ ",")?
| ""
}
lambda = {
"|" ~ whitespace* ~ formal_parameters ~ whitespace* ~ "|"
~ whitespace* ~ template
}
formal_parameters = {
identifier ~ (whitespace* ~ "," ~ whitespace* ~ identifier)* ~ (whitespace* ~ ",")?
| ""
@ -44,6 +48,7 @@ formal_parameters = {
primary = _{
("(" ~ whitespace* ~ template ~ whitespace* ~ ")")
| function
| lambda
| identifier
| literal
| integer_literal

View File

@ -557,6 +557,10 @@ pub fn build_expression<'a, L: TemplateLanguage<'a>>(
}
ExpressionKind::FunctionCall(function) => build_global_function(language, function),
ExpressionKind::MethodCall(method) => build_method_call(language, method),
ExpressionKind::Lambda(_) => Err(TemplateParseError::unexpected_expression(
"Lambda cannot be defined here",
node.span,
)),
ExpressionKind::AliasExpanded(id, subst) => {
build_expression(language, subst).map_err(|e| e.within_alias_expansion(*id, node.span))
}

View File

@ -200,6 +200,7 @@ pub enum ExpressionKind<'i> {
Concat(Vec<ExpressionNode<'i>>),
FunctionCall(FunctionCallNode<'i>),
MethodCall(MethodCallNode<'i>),
Lambda(LambdaNode<'i>),
/// Identity node to preserve the span in the source template text.
AliasExpanded(TemplateAliasId<'i>, Box<ExpressionNode<'i>>),
}
@ -218,6 +219,13 @@ pub struct MethodCallNode<'i> {
pub function: FunctionCallNode<'i>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct LambdaNode<'i> {
pub params: Vec<&'i str>,
pub params_span: pest::Span<'i>,
pub body: Box<ExpressionNode<'i>>,
}
fn parse_string_literal(pair: Pair<Rule>) -> String {
assert_eq!(pair.as_rule(), Rule::literal);
let mut result = String::new();
@ -240,6 +248,26 @@ fn parse_string_literal(pair: Pair<Rule>) -> String {
result
}
fn parse_formal_parameters(params_pair: Pair<Rule>) -> TemplateParseResult<Vec<&str>> {
assert_eq!(params_pair.as_rule(), Rule::formal_parameters);
let params_span = params_pair.as_span();
let params = params_pair
.into_inner()
.map(|pair| match pair.as_rule() {
Rule::identifier => pair.as_str(),
r => panic!("unexpected formal parameter rule {r:?}"),
})
.collect_vec();
if params.iter().all_unique() {
Ok(params)
} else {
Err(TemplateParseError::with_span(
TemplateParseErrorKind::RedefinedFunctionParameter,
params_span,
))
}
}
fn parse_function_call_node(pair: Pair<Rule>) -> TemplateParseResult<FunctionCallNode> {
assert_eq!(pair.as_rule(), Rule::function);
let mut inner = pair.into_inner();
@ -260,6 +288,21 @@ fn parse_function_call_node(pair: Pair<Rule>) -> TemplateParseResult<FunctionCal
})
}
fn parse_lambda_node(pair: Pair<Rule>) -> TemplateParseResult<LambdaNode> {
assert_eq!(pair.as_rule(), Rule::lambda);
let mut inner = pair.into_inner();
let params_pair = inner.next().unwrap();
let params_span = params_pair.as_span();
let body_pair = inner.next().unwrap();
let params = parse_formal_parameters(params_pair)?;
let body = parse_template_node(body_pair)?;
Ok(LambdaNode {
params,
params_span,
body: Box::new(body),
})
}
fn parse_term_node(pair: Pair<Rule>) -> TemplateParseResult<ExpressionNode> {
assert_eq!(pair.as_rule(), Rule::term);
let mut inner = pair.into_inner();
@ -281,6 +324,10 @@ fn parse_term_node(pair: Pair<Rule>) -> TemplateParseResult<ExpressionNode> {
let function = parse_function_call_node(expr)?;
ExpressionNode::new(ExpressionKind::FunctionCall(function), span)
}
Rule::lambda => {
let lambda = parse_lambda_node(expr)?;
ExpressionNode::new(ExpressionKind::Lambda(lambda), span)
}
Rule::template => parse_template_node(expr)?,
other => panic!("unexpected term: {other:?}"),
};
@ -389,25 +436,13 @@ impl TemplateAliasDeclaration {
let mut inner = first.into_inner();
let name_pair = inner.next().unwrap();
let params_pair = inner.next().unwrap();
let params_span = params_pair.as_span();
assert_eq!(name_pair.as_rule(), Rule::identifier);
assert_eq!(params_pair.as_rule(), Rule::formal_parameters);
let name = name_pair.as_str().to_owned();
let params = params_pair
.into_inner()
.map(|pair| match pair.as_rule() {
Rule::identifier => pair.as_str().to_owned(),
r => panic!("unexpected formal parameter rule {r:?}"),
})
.collect_vec();
if params.iter().all_unique() {
Ok(TemplateAliasDeclaration::Function(name, params))
} else {
Err(TemplateParseError::with_span(
TemplateParseErrorKind::RedefinedFunctionParameter,
params_span,
))
}
let params = parse_formal_parameters(params_pair)?
.into_iter()
.map(|s| s.to_owned())
.collect();
Ok(TemplateAliasDeclaration::Function(name, params))
}
r => panic!("unexpected alias declaration rule {r:?}"),
}
@ -541,6 +576,14 @@ pub fn expand_aliases<'i>(
});
Ok(node)
}
ExpressionKind::Lambda(lambda) => {
node.kind = ExpressionKind::Lambda(LambdaNode {
params: lambda.params,
params_span: lambda.params_span,
body: Box::new(expand_node(*lambda.body, state)?),
});
Ok(node)
}
ExpressionKind::AliasExpanded(id, subst) => {
// Just in case the original tree contained AliasExpanded node.
let subst = Box::new(expand_node(*subst, state)?);
@ -636,7 +679,8 @@ pub fn expect_string_literal_with<'a, 'i, T>(
| ExpressionKind::Integer(_)
| ExpressionKind::Concat(_)
| ExpressionKind::FunctionCall(_)
| ExpressionKind::MethodCall(_) => Err(TemplateParseError::unexpected_expression(
| ExpressionKind::MethodCall(_)
| ExpressionKind::Lambda(_) => Err(TemplateParseError::unexpected_expression(
"Expected string literal",
node.span,
)),
@ -719,6 +763,14 @@ mod tests {
let function = normalize_function_call(method.function);
ExpressionKind::MethodCall(MethodCallNode { object, function })
}
ExpressionKind::Lambda(lambda) => {
let body = Box::new(normalize_tree(*lambda.body));
ExpressionKind::Lambda(LambdaNode {
params: lambda.params,
params_span: empty_span(),
body,
})
}
ExpressionKind::AliasExpanded(_, subst) => normalize_tree(*subst).kind,
};
ExpressionNode {
@ -772,6 +824,55 @@ mod tests {
assert!(parse_template(r#" label("",,"") "#).is_err());
}
#[test]
fn test_lambda_syntax() {
fn unwrap_lambda(node: ExpressionNode<'_>) -> LambdaNode<'_> {
match node.kind {
ExpressionKind::Lambda(lambda) => lambda,
_ => panic!("unexpected expression: {node:?}"),
}
}
let lambda = unwrap_lambda(parse_template("|| a").unwrap());
assert_eq!(lambda.params.len(), 0);
assert_eq!(lambda.body.kind, ExpressionKind::Identifier("a"));
let lambda = unwrap_lambda(parse_template("|foo| a").unwrap());
assert_eq!(lambda.params.len(), 1);
let lambda = unwrap_lambda(parse_template("|foo, b| a").unwrap());
assert_eq!(lambda.params.len(), 2);
// No body
assert!(parse_template("||").is_err());
// Binding
assert_eq!(
parse_normalized("|| x ++ y").unwrap(),
parse_normalized("|| (x ++ y)").unwrap(),
);
assert_eq!(
parse_normalized("f( || x, || y)").unwrap(),
parse_normalized("f((|| x), (|| y))").unwrap(),
);
assert_eq!(
parse_normalized("|| x ++ || y").unwrap(),
parse_normalized("|| (x ++ (|| y))").unwrap(),
);
// Trailing comma
assert!(parse_template("|,| a").is_err());
assert!(parse_template("|x,| a").is_ok());
assert!(parse_template("|x , | a").is_ok());
assert!(parse_template("|,x| a").is_err());
assert!(parse_template("| x,y,| a").is_ok());
assert!(parse_template("|x,,y| a").is_err());
// Formal parameter can't be redefined
assert_eq!(
parse_template("|x, x| a").unwrap_err().kind,
TemplateParseErrorKind::RedefinedFunctionParameter
);
}
#[test]
fn test_string_literal() {
// "\<char>" escapes
@ -876,6 +977,21 @@ mod tests {
parse_normalized("x.f(a, b)").unwrap(),
);
// Lambda expression body should be expanded.
assert_eq!(
with_aliases([("A", "a")]).parse_normalized("|| A").unwrap(),
parse_normalized("|| a").unwrap(),
);
// No matter if 'A' is a formal parameter. Alias substitution isn't scoped.
// If we don't like this behavior, maybe we can turn off alias substitution
// for lambda parameters.
assert_eq!(
with_aliases([("A", "a ++ b")])
.parse_normalized("|A| A")
.unwrap(),
parse_normalized("|A| (a ++ b)").unwrap(),
);
// Infinite recursion, where the top-level error isn't of RecursiveAlias kind.
assert_eq!(
with_aliases([("A", "A")]).parse("A").unwrap_err().kind,
@ -971,6 +1087,15 @@ mod tests {
parse_normalized("x.F()").unwrap(),
);
// Formal parameter shouldn't be substituted by alias parameter, but
// the expression should be substituted.
assert_eq!(
with_aliases([("F(x)", "|x| x")])
.parse_normalized("F(a ++ b)")
.unwrap(),
parse_normalized("|x| (a ++ b)").unwrap(),
);
// Invalid number of arguments.
assert_matches!(
with_aliases([("F()", "x")]).parse("F(a)").unwrap_err().kind,

View File

@ -234,6 +234,15 @@ fn test_templater_parse_error() {
|
= Expected expression of type "Boolean"
"###);
insta::assert_snapshot!(render_err(r#"|x| description"#), @r###"
Error: Failed to parse template: --> 1:1
|
1 | |x| description
| ^-------------^
|
= Lambda cannot be defined here
"###);
}
#[test]