mirror of
https://github.com/martinvonz/jj.git
synced 2025-05-20 06:33:14 +00:00
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:
parent
e2b4d7058d
commit
1c0bde1a2b
@ -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
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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]
|
||||
|
Loading…
x
Reference in New Issue
Block a user