From 9d356b80947910246ce5ea1838236cdfd3899b36 Mon Sep 17 00:00:00 2001 From: Yuya Nishihara Date: Sun, 12 Feb 2023 17:21:06 +0900 Subject: [PATCH] templater: add table of alias (decl, defn) rules This is basically a copy of revset::RevsetAliasesMap. We could extract a common table struct, but that wouldn't be worth the effort since the core alias substitution logic can't be easily abstracted. --- src/template.pest | 11 +++ src/template_parser.rs | 148 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 159 insertions(+) diff --git a/src/template.pest b/src/template.pest index 039d66fc9..cbcdc1715 100644 --- a/src/template.pest +++ b/src/template.pest @@ -36,6 +36,10 @@ function_arguments = { template ~ (whitespace* ~ "," ~ whitespace* ~ template)* ~ (whitespace* ~ ",")? | "" } +formal_parameters = { + identifier ~ (whitespace* ~ "," ~ whitespace* ~ identifier)* ~ (whitespace* ~ ",")? + | "" +} // Note that "x(y)" is a function call but "x (y)" concatenates "x" and "y" primary = _{ @@ -57,3 +61,10 @@ list = _{ template = { list | term } program = _{ SOI ~ whitespace* ~ template? ~ whitespace* ~ EOI } + +function_alias_declaration = { + identifier ~ "(" ~ whitespace* ~ formal_parameters ~ whitespace* ~ ")" +} +alias_declaration = _{ + SOI ~ (function_alias_declaration | identifier) ~ EOI +} diff --git a/src/template_parser.rs b/src/template_parser.rs index 2bd2abedc..661b5c221 100644 --- a/src/template_parser.rs +++ b/src/template_parser.rs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +use std::collections::HashMap; use std::num::ParseIntError; use std::ops::{RangeFrom, RangeInclusive}; use std::{error, fmt}; @@ -68,6 +69,8 @@ pub enum TemplateParseErrorKind { InvalidArgumentCountRangeFrom(RangeFrom), #[error(r#"Expected argument of type "{0}""#)] InvalidArgumentType(String), + #[error("Redefinition of function parameter")] + RedefinedFunctionParameter, } impl TemplateParseError { @@ -296,6 +299,118 @@ pub fn parse_template(template_text: &str) -> TemplateParseResult, + function_aliases: HashMap, String)>, +} + +impl TemplateAliasesMap { + pub fn new() -> Self { + Self::default() + } + + /// Adds new substitution rule `decl = defn`. + /// + /// Returns error if `decl` is invalid. The `defn` part isn't checked. A bad + /// `defn` will be reported when the alias is substituted. + pub fn insert( + &mut self, + decl: impl AsRef, + defn: impl Into, + ) -> TemplateParseResult<()> { + match TemplateAliasDeclaration::parse(decl.as_ref())? { + TemplateAliasDeclaration::Symbol(name) => { + self.symbol_aliases.insert(name, defn.into()); + } + TemplateAliasDeclaration::Function(name, params) => { + self.function_aliases.insert(name, (params, defn.into())); + } + } + Ok(()) + } + + #[cfg(test)] // TODO: remove + fn get_symbol(&self, name: &str) -> Option<(TemplateAliasId<'_>, &str)> { + self.symbol_aliases + .get_key_value(name) + .map(|(name, defn)| (TemplateAliasId::Symbol(name), defn.as_ref())) + } + + #[cfg(test)] // TODO: remove + fn get_function(&self, name: &str) -> Option<(TemplateAliasId<'_>, &[String], &str)> { + self.function_aliases + .get_key_value(name) + .map(|(name, (params, defn))| { + ( + TemplateAliasId::Function(name), + params.as_ref(), + defn.as_ref(), + ) + }) + } +} + +/// Parsed declaration part of alias rule. +#[derive(Clone, Debug)] +enum TemplateAliasDeclaration { + Symbol(String), + Function(String, Vec), +} + +impl TemplateAliasDeclaration { + fn parse(source: &str) -> TemplateParseResult { + let mut pairs = TemplateParser::parse(Rule::alias_declaration, source)?; + let first = pairs.next().unwrap(); + match first.as_rule() { + Rule::identifier => Ok(TemplateAliasDeclaration::Symbol(first.as_str().to_owned())), + Rule::function_alias_declaration => { + 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, + )) + } + } + r => panic!("unexpected alias declaration rule {r:?}"), + } + } +} + +/// Borrowed reference to identify alias expression. +#[cfg(test)] // TODO: remove +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum TemplateAliasId<'a> { + Symbol(&'a str), + Function(&'a str), +} + +#[cfg(test)] // TODO: remove +impl fmt::Display for TemplateAliasId<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + TemplateAliasId::Symbol(name) => write!(f, "{name}"), + TemplateAliasId::Function(name) => write!(f, "{name}()"), + } + } +} + enum Property<'a, I> { String(Box + 'a>), Boolean(Box + 'a>), @@ -926,4 +1041,37 @@ mod tests { assert_eq!(extract(parse(&format!("{}", i64::MAX)).unwrap()), i64::MAX); assert!(parse(&format!("{}", (i64::MAX as u64) + 1)).is_err()); } + + #[test] + fn test_parse_alias_decl() { + let mut aliases_map = TemplateAliasesMap::new(); + aliases_map.insert("sym", r#""is symbol""#).unwrap(); + aliases_map.insert("func(a)", r#""is function""#).unwrap(); + + let (id, defn) = aliases_map.get_symbol("sym").unwrap(); + assert_eq!(id, TemplateAliasId::Symbol("sym")); + assert_eq!(defn, r#""is symbol""#); + + let (id, params, defn) = aliases_map.get_function("func").unwrap(); + assert_eq!(id, TemplateAliasId::Function("func")); + assert_eq!(params, ["a"]); + assert_eq!(defn, r#""is function""#); + + // Formal parameter 'a' can't be redefined + assert_eq!( + aliases_map.insert("f(a, a)", r#""""#).unwrap_err().kind, + TemplateParseErrorKind::RedefinedFunctionParameter + ); + + // Trailing comma isn't allowed for empty parameter + assert!(aliases_map.insert("f(,)", r#"""#).is_err()); + // Trailing comma is allowed for the last parameter + assert!(aliases_map.insert("g(a,)", r#"""#).is_ok()); + assert!(aliases_map.insert("h(a , )", r#"""#).is_ok()); + assert!(aliases_map.insert("i(,a)", r#"""#).is_err()); + assert!(aliases_map.insert("j(a,,)", r#"""#).is_err()); + assert!(aliases_map.insert("k(a , , )", r#"""#).is_err()); + assert!(aliases_map.insert("l(a,b,)", r#"""#).is_ok()); + assert!(aliases_map.insert("m(a,,b)", r#"""#).is_err()); + } }