diff --git a/GRAMMAR.md b/GRAMMAR.md index e5847f50..00721f15 100644 --- a/GRAMMAR.md +++ b/GRAMMAR.md @@ -90,7 +90,13 @@ import : 'import' '?'? string? eol module : 'mod' '?'? NAME string? eol -expression : 'if' condition '{' expression '}' 'else' '{' expression '}' +expression : disjunct || expression + | disjunct + +disjunct : conjunct && disjunct + | conjunct + +conjunct : 'if' condition '{' expression '}' 'else' '{' expression '}' | 'assert' '(' condition ',' expression ')' | '/' expression | value '/' expression diff --git a/README.md b/README.md index ca596854..bccf9a13 100644 --- a/README.md +++ b/README.md @@ -1290,9 +1290,11 @@ Available recipes: test ``` -### Variables and Substitution +### Expressions and Substitutions -Variables, strings, concatenation, path joining, substitution using `{{…}}`, and function calls are supported: +Various operators and function calls are supported in expressions, which may be +used in assignments, default recipe arguments, and inside recipe body `{{…}}` +substitutions. ```just tmpdir := `mktemp -d` @@ -1310,6 +1312,39 @@ publish: rm -rf {{tarball}} {{tardir}} ``` +#### Concatenation + +The `+` operator returns the left-hand argument concatenated with the +right-hand argument: + +```just +foobar := 'foo' + 'bar' +``` + +#### Logical Operators + +The logical operators `&&` and `||` can be used to coalesce string +valuesmaster, similar to Python's `and` and `or`. These operators +consider the empty string `''` to be false, and all other strings to be true. + +These operators are currently unstable. + +The `&&` operator returns the empty string if the left-hand argument is the +empty string, otherwise it returns the right-hand argument: + +```mf +foo := '' && 'goodbye' # '' +bar := 'hello' && 'goodbye' # 'goodbye' +``` + +The `||` operator returns the left-hand argument if it is non-empty, otherwise +it returns the right-hand argument: + +```mf +foo := '' || 'goodbye' # 'goodbye' +bar := 'hello' || 'goodbye' # 'hello' +``` + #### Joining Paths The `/` operator can be used to join two strings with a slash: @@ -2367,8 +2402,8 @@ Testing server:unit… ./test --tests unit server ``` -Default values may be arbitrary expressions, but concatenations or path joins -must be parenthesized: +Default values may be arbitrary expressions, but expressions containing the +`+`, `&&`, `||`, or `/` operators must be parenthesized: ```just arch := "wasm" diff --git a/src/analyzer.rs b/src/analyzer.rs index 9a26ef04..0929294c 100644 --- a/src/analyzer.rs +++ b/src/analyzer.rs @@ -36,12 +36,15 @@ impl<'run, 'src> Analyzer<'run, 'src> { ) -> CompileResult<'src, Justfile<'src>> { let mut definitions = HashMap::new(); let mut imports = HashSet::new(); + let mut unstable_features = BTreeSet::new(); let mut stack = Vec::new(); let ast = asts.get(root).unwrap(); stack.push(ast); while let Some(ast) = stack.pop() { + unstable_features.extend(&ast.unstable_features); + for item in &ast.items { match item { Item::Alias(alias) => { @@ -166,8 +169,6 @@ impl<'run, 'src> Analyzer<'run, 'src> { aliases.insert(Self::resolve_alias(&recipes, alias)?); } - let mut unstable_features = BTreeSet::new(); - for recipe in recipes.values() { for attribute in &recipe.attributes { if let Attribute::Script(_) = attribute { diff --git a/src/assignment_resolver.rs b/src/assignment_resolver.rs index 53863fc9..511464d4 100644 --- a/src/assignment_resolver.rs +++ b/src/assignment_resolver.rs @@ -31,7 +31,29 @@ impl<'src: 'run, 'run> AssignmentResolver<'src, 'run> { self.stack.push(name); if let Some(assignment) = self.assignments.get(name) { - self.resolve_expression(&assignment.value)?; + for variable in assignment.value.variables() { + let name = variable.lexeme(); + + if self.evaluated.contains(name) || constants().contains_key(name) { + continue; + } + + if self.stack.contains(&name) { + self.stack.push(name); + return Err( + self.assignments[name] + .name + .error(CircularVariableDependency { + variable: name, + circle: self.stack.clone(), + }), + ); + } else if self.assignments.contains_key(name) { + self.resolve_assignment(name)?; + } else { + return Err(variable.error(UndefinedVariable { variable: name })); + } + } self.evaluated.insert(name); } else { let message = format!("attempted to resolve unknown assignment `{name}`"); @@ -51,112 +73,6 @@ impl<'src: 'run, 'run> AssignmentResolver<'src, 'run> { Ok(()) } - - fn resolve_expression(&mut self, expression: &Expression<'src>) -> CompileResult<'src> { - match expression { - Expression::Assert { - condition: Condition { - lhs, - rhs, - operator: _, - }, - error, - } => { - self.resolve_expression(lhs)?; - self.resolve_expression(rhs)?; - self.resolve_expression(error) - } - Expression::Call { thunk } => match thunk { - Thunk::Nullary { .. } => Ok(()), - Thunk::Unary { arg, .. } => self.resolve_expression(arg), - Thunk::UnaryOpt { args: (a, b), .. } => { - self.resolve_expression(a)?; - if let Some(b) = b.as_ref() { - self.resolve_expression(b)?; - } - Ok(()) - } - Thunk::UnaryPlus { - args: (a, rest), .. - } => { - self.resolve_expression(a)?; - for arg in rest { - self.resolve_expression(arg)?; - } - Ok(()) - } - Thunk::Binary { args: [a, b], .. } => { - self.resolve_expression(a)?; - self.resolve_expression(b) - } - Thunk::BinaryPlus { - args: ([a, b], rest), - .. - } => { - self.resolve_expression(a)?; - self.resolve_expression(b)?; - for arg in rest { - self.resolve_expression(arg)?; - } - Ok(()) - } - Thunk::Ternary { - args: [a, b, c], .. - } => { - self.resolve_expression(a)?; - self.resolve_expression(b)?; - self.resolve_expression(c) - } - }, - Expression::Concatenation { lhs, rhs } => { - self.resolve_expression(lhs)?; - self.resolve_expression(rhs) - } - Expression::Conditional { - condition: Condition { - lhs, - rhs, - operator: _, - }, - then, - otherwise, - .. - } => { - self.resolve_expression(lhs)?; - self.resolve_expression(rhs)?; - self.resolve_expression(then)?; - self.resolve_expression(otherwise) - } - Expression::Group { contents } => self.resolve_expression(contents), - Expression::Join { lhs, rhs } => { - if let Some(lhs) = lhs { - self.resolve_expression(lhs)?; - } - self.resolve_expression(rhs) - } - Expression::StringLiteral { .. } | Expression::Backtick { .. } => Ok(()), - Expression::Variable { name } => { - let variable = name.lexeme(); - if self.evaluated.contains(variable) || constants().contains_key(variable) { - Ok(()) - } else if self.stack.contains(&variable) { - self.stack.push(variable); - Err( - self.assignments[variable] - .name - .error(CircularVariableDependency { - variable, - circle: self.stack.clone(), - }), - ) - } else if self.assignments.contains_key(variable) { - self.resolve_assignment(variable) - } else { - Err(name.token.error(UndefinedVariable { variable })) - } - } - } - } } #[cfg(test)] diff --git a/src/ast.rs b/src/ast.rs index f9dd10c9..1ad7a8aa 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -6,6 +6,7 @@ use super::*; #[derive(Debug, Clone)] pub(crate) struct Ast<'src> { pub(crate) items: Vec>, + pub(crate) unstable_features: BTreeSet, pub(crate) warnings: Vec, pub(crate) working_directory: PathBuf, } diff --git a/src/evaluator.rs b/src/evaluator.rs index 4ed00036..d48cece8 100644 --- a/src/evaluator.rs +++ b/src/evaluator.rs @@ -84,24 +84,31 @@ impl<'src, 'run> Evaluator<'src, 'run> { expression: &Expression<'src>, ) -> RunResult<'src, String> { match expression { - Expression::Variable { name, .. } => { - let variable = name.lexeme(); - if let Some(value) = self.scope.value(variable) { - Ok(value.to_owned()) - } else if let Some(assignment) = self - .assignments - .and_then(|assignments| assignments.get(variable)) - { - Ok(self.evaluate_assignment(assignment)?.to_owned()) + Expression::And { lhs, rhs } => { + let lhs = self.evaluate_expression(lhs)?; + if lhs.is_empty() { + return Ok(String::new()); + } + self.evaluate_expression(rhs) + } + Expression::Assert { condition, error } => { + if self.evaluate_condition(condition)? { + Ok(String::new()) } else { - Err(Error::Internal { - message: format!("attempted to evaluate undefined variable `{variable}`"), + Err(Error::Assert { + message: self.evaluate_expression(error)?, }) } } + Expression::Backtick { contents, token } => { + if self.context.config.dry_run { + Ok(format!("`{contents}`")) + } else { + Ok(self.run_backtick(contents, token)?) + } + } Expression::Call { thunk } => { use Thunk::*; - let result = match thunk { Nullary { function, .. } => function(function::Context::new(self, thunk.name())), Unary { function, arg, .. } => { @@ -118,7 +125,6 @@ impl<'src, 'run> Evaluator<'src, 'run> { Some(b) => Some(self.evaluate_expression(b)?), None => None, }; - function(function::Context::new(self, thunk.name()), &a, b.as_deref()) } UnaryPlus { @@ -175,20 +181,11 @@ impl<'src, 'run> Evaluator<'src, 'run> { function(function::Context::new(self, thunk.name()), &a, &b, &c) } }; - result.map_err(|message| Error::FunctionCall { function: thunk.name(), message, }) } - Expression::StringLiteral { string_literal } => Ok(string_literal.cooked.clone()), - Expression::Backtick { contents, token } => { - if self.context.config.dry_run { - Ok(format!("`{contents}`")) - } else { - Ok(self.run_backtick(contents, token)?) - } - } Expression::Concatenation { lhs, rhs } => { Ok(self.evaluate_expression(lhs)? + &self.evaluate_expression(rhs)?) } @@ -209,12 +206,26 @@ impl<'src, 'run> Evaluator<'src, 'run> { lhs: Some(lhs), rhs, } => Ok(self.evaluate_expression(lhs)? + "/" + &self.evaluate_expression(rhs)?), - Expression::Assert { condition, error } => { - if self.evaluate_condition(condition)? { - Ok(String::new()) + Expression::Or { lhs, rhs } => { + let lhs = self.evaluate_expression(lhs)?; + if !lhs.is_empty() { + return Ok(lhs); + } + self.evaluate_expression(rhs) + } + Expression::StringLiteral { string_literal } => Ok(string_literal.cooked.clone()), + Expression::Variable { name, .. } => { + let variable = name.lexeme(); + if let Some(value) = self.scope.value(variable) { + Ok(value.to_owned()) + } else if let Some(assignment) = self + .assignments + .and_then(|assignments| assignments.get(variable)) + { + Ok(self.evaluate_assignment(assignment)?.to_owned()) } else { - Err(Error::Assert { - message: self.evaluate_expression(error)?, + Err(Error::Internal { + message: format!("attempted to evaluate undefined variable `{variable}`"), }) } } diff --git a/src/expression.rs b/src/expression.rs index c1e4b5e0..3d5c3392 100644 --- a/src/expression.rs +++ b/src/expression.rs @@ -8,6 +8,11 @@ use super::*; /// The parser parses both values and expressions into `Expression`s. #[derive(PartialEq, Debug, Clone)] pub(crate) enum Expression<'src> { + /// `lhs && rhs` + And { + lhs: Box>, + rhs: Box>, + }, /// `assert(condition, error)` Assert { condition: Condition<'src>, @@ -38,6 +43,11 @@ pub(crate) enum Expression<'src> { lhs: Option>>, rhs: Box>, }, + /// `lhs || rhs` + Or { + lhs: Box>, + rhs: Box>, + }, /// `"string_literal"` or `'string_literal'` StringLiteral { string_literal: StringLiteral<'src> }, /// `variable` @@ -53,23 +63,25 @@ impl<'src> Expression<'src> { impl<'src> Display for Expression<'src> { fn fmt(&self, f: &mut Formatter) -> fmt::Result { match self { + Self::And { lhs, rhs } => write!(f, "{lhs} && {rhs}"), Self::Assert { condition, error } => write!(f, "assert({condition}, {error})"), Self::Backtick { token, .. } => write!(f, "{}", token.lexeme()), - Self::Join { lhs: None, rhs } => write!(f, "/ {rhs}"), - Self::Join { - lhs: Some(lhs), - rhs, - } => write!(f, "{lhs} / {rhs}"), + Self::Call { thunk } => write!(f, "{thunk}"), Self::Concatenation { lhs, rhs } => write!(f, "{lhs} + {rhs}"), Self::Conditional { condition, then, otherwise, } => write!(f, "if {condition} {{ {then} }} else {{ {otherwise} }}"), + Self::Group { contents } => write!(f, "({contents})"), + Self::Join { lhs: None, rhs } => write!(f, "/ {rhs}"), + Self::Join { + lhs: Some(lhs), + rhs, + } => write!(f, "{lhs} / {rhs}"), + Self::Or { lhs, rhs } => write!(f, "{lhs} || {rhs}"), Self::StringLiteral { string_literal } => write!(f, "{string_literal}"), Self::Variable { name } => write!(f, "{}", name.lexeme()), - Self::Call { thunk } => write!(f, "{thunk}"), - Self::Group { contents } => write!(f, "({contents})"), } } } @@ -80,6 +92,13 @@ impl<'src> Serialize for Expression<'src> { S: Serializer, { match self { + Self::And { lhs, rhs } => { + let mut seq = serializer.serialize_seq(None)?; + seq.serialize_element("and")?; + seq.serialize_element(lhs)?; + seq.serialize_element(rhs)?; + seq.end() + } Self::Assert { condition, error } => { let mut seq: ::SerializeSeq = serializer.serialize_seq(None)?; seq.serialize_element("assert")?; @@ -101,13 +120,6 @@ impl<'src> Serialize for Expression<'src> { seq.serialize_element(rhs)?; seq.end() } - Self::Join { lhs, rhs } => { - let mut seq = serializer.serialize_seq(None)?; - seq.serialize_element("join")?; - seq.serialize_element(lhs)?; - seq.serialize_element(rhs)?; - seq.end() - } Self::Conditional { condition, then, @@ -121,6 +133,20 @@ impl<'src> Serialize for Expression<'src> { seq.end() } Self::Group { contents } => contents.serialize(serializer), + Self::Join { lhs, rhs } => { + let mut seq = serializer.serialize_seq(None)?; + seq.serialize_element("join")?; + seq.serialize_element(lhs)?; + seq.serialize_element(rhs)?; + seq.end() + } + Self::Or { lhs, rhs } => { + let mut seq = serializer.serialize_seq(None)?; + seq.serialize_element("or")?; + seq.serialize_element(lhs)?; + seq.serialize_element(rhs)?; + seq.end() + } Self::StringLiteral { string_literal } => string_literal.serialize(serializer), Self::Variable { name } => { let mut seq = serializer.serialize_seq(None)?; diff --git a/src/lexer.rs b/src/lexer.rs index 4d28a446..2c56db9d 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -496,6 +496,7 @@ impl<'src> Lexer<'src> { ']' => self.lex_delimiter(BracketR), '`' | '"' | '\'' => self.lex_string(), '{' => self.lex_delimiter(BraceL), + '|' => self.lex_digraph('|', '|', BarBar), '}' => self.lex_delimiter(BraceR), _ if Self::is_identifier_start(start) => self.lex_identifier(), _ => { @@ -948,6 +949,7 @@ mod tests { Asterisk => "*", At => "@", BangEquals => "!=", + BarBar => "||", BraceL => "{", BraceR => "}", BracketL => "[", diff --git a/src/node.rs b/src/node.rs index 3ccf862d..f8788c4d 100644 --- a/src/node.rs +++ b/src/node.rs @@ -88,6 +88,7 @@ impl<'src> Node<'src> for Assignment<'src> { impl<'src> Node<'src> for Expression<'src> { fn tree(&self) -> Tree<'src> { match self { + Self::And { lhs, rhs } => Tree::atom("&&").push(lhs.tree()).push(rhs.tree()), Self::Assert { condition: Condition { lhs, rhs, operator }, error, @@ -96,25 +97,10 @@ impl<'src> Node<'src> for Expression<'src> { .push(operator.to_string()) .push(rhs.tree()) .push(error.tree()), - Self::Concatenation { lhs, rhs } => Tree::atom("+").push(lhs.tree()).push(rhs.tree()), - Self::Conditional { - condition: Condition { lhs, rhs, operator }, - then, - otherwise, - } => { - let mut tree = Tree::atom(Keyword::If.lexeme()); - tree.push_mut(lhs.tree()); - tree.push_mut(operator.to_string()); - tree.push_mut(rhs.tree()); - tree.push_mut(then.tree()); - tree.push_mut(otherwise.tree()); - tree - } + Self::Backtick { contents, .. } => Tree::atom("backtick").push(Tree::string(contents)), Self::Call { thunk } => { use Thunk::*; - let mut tree = Tree::atom("call"); - match thunk { Nullary { name, .. } => tree.push_mut(name.lexeme()), Unary { name, arg, .. } => { @@ -171,20 +157,33 @@ impl<'src> Node<'src> for Expression<'src> { tree.push_mut(c.tree()); } } - tree } - Self::Variable { name } => Tree::atom(name.lexeme()), - Self::StringLiteral { - string_literal: StringLiteral { cooked, .. }, - } => Tree::string(cooked), - Self::Backtick { contents, .. } => Tree::atom("backtick").push(Tree::string(contents)), + Self::Concatenation { lhs, rhs } => Tree::atom("+").push(lhs.tree()).push(rhs.tree()), + Self::Conditional { + condition: Condition { lhs, rhs, operator }, + then, + otherwise, + } => { + let mut tree = Tree::atom(Keyword::If.lexeme()); + tree.push_mut(lhs.tree()); + tree.push_mut(operator.to_string()); + tree.push_mut(rhs.tree()); + tree.push_mut(then.tree()); + tree.push_mut(otherwise.tree()); + tree + } Self::Group { contents } => Tree::List(vec![contents.tree()]), Self::Join { lhs: None, rhs } => Tree::atom("/").push(rhs.tree()), Self::Join { lhs: Some(lhs), rhs, } => Tree::atom("/").push(lhs.tree()).push(rhs.tree()), + Self::Or { lhs, rhs } => Tree::atom("||").push(lhs.tree()).push(rhs.tree()), + Self::StringLiteral { + string_literal: StringLiteral { cooked, .. }, + } => Tree::string(cooked), + Self::Variable { name } => Tree::atom(name.lexeme()), } } } diff --git a/src/parser.rs b/src/parser.rs index 99544acd..e03ab783 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -31,6 +31,7 @@ pub(crate) struct Parser<'run, 'src> { next_token: usize, recursion_depth: usize, tokens: &'run [Token<'src>], + unstable_features: BTreeSet, working_directory: &'run Path, } @@ -51,6 +52,7 @@ impl<'run, 'src> Parser<'run, 'src> { next_token: 0, recursion_depth: 0, tokens, + unstable_features: BTreeSet::new(), working_directory, } .parse_ast() @@ -442,18 +444,19 @@ impl<'run, 'src> Parser<'run, 'src> { } } - if self.next_token == self.tokens.len() { - Ok(Ast { - items, - warnings: Vec::new(), - working_directory: self.working_directory.into(), - }) - } else { - Err(self.internal_error(format!( + if self.next_token != self.tokens.len() { + return Err(self.internal_error(format!( "Parse completed with {} unparsed tokens", self.tokens.len() - self.next_token, - ))?) + ))?); } + + Ok(Ast { + items, + unstable_features: self.unstable_features, + warnings: Vec::new(), + working_directory: self.working_directory.into(), + }) } /// Parse an alias, e.g `alias name := target` @@ -517,26 +520,17 @@ impl<'run, 'src> Parser<'run, 'src> { self.recursion_depth += 1; - let expression = if self.accepted_keyword(Keyword::If)? { - self.parse_conditional()? - } else if self.accepted(Slash)? { - let lhs = None; - let rhs = self.parse_expression()?.into(); - Expression::Join { lhs, rhs } - } else { - let value = self.parse_value()?; + let disjunct = self.parse_disjunct()?; - if self.accepted(Slash)? { - let lhs = Some(Box::new(value)); - let rhs = self.parse_expression()?.into(); - Expression::Join { lhs, rhs } - } else if self.accepted(Plus)? { - let lhs = value.into(); - let rhs = self.parse_expression()?.into(); - Expression::Concatenation { lhs, rhs } - } else { - value - } + let expression = if self.accepted(BarBar)? { + self + .unstable_features + .insert(UnstableFeature::LogicalOperators); + let lhs = disjunct.into(); + let rhs = self.parse_expression()?.into(); + Expression::Or { lhs, rhs } + } else { + disjunct }; self.recursion_depth -= 1; @@ -544,6 +538,47 @@ impl<'run, 'src> Parser<'run, 'src> { Ok(expression) } + fn parse_disjunct(&mut self) -> CompileResult<'src, Expression<'src>> { + let conjunct = self.parse_conjunct()?; + + let disjunct = if self.accepted(AmpersandAmpersand)? { + self + .unstable_features + .insert(UnstableFeature::LogicalOperators); + let lhs = conjunct.into(); + let rhs = self.parse_disjunct()?.into(); + Expression::And { lhs, rhs } + } else { + conjunct + }; + + Ok(disjunct) + } + + fn parse_conjunct(&mut self) -> CompileResult<'src, Expression<'src>> { + if self.accepted_keyword(Keyword::If)? { + self.parse_conditional() + } else if self.accepted(Slash)? { + let lhs = None; + let rhs = self.parse_conjunct()?.into(); + Ok(Expression::Join { lhs, rhs }) + } else { + let value = self.parse_value()?; + + if self.accepted(Slash)? { + let lhs = Some(Box::new(value)); + let rhs = self.parse_conjunct()?.into(); + Ok(Expression::Join { lhs, rhs }) + } else if self.accepted(Plus)? { + let lhs = value.into(); + let rhs = self.parse_conjunct()?.into(); + Ok(Expression::Concatenation { lhs, rhs }) + } else { + Ok(value) + } + } + } + /// Parse a conditional, e.g. `if a == b { "foo" } else { "bar" }` fn parse_conditional(&mut self) -> CompileResult<'src, Expression<'src>> { let condition = self.parse_condition()?; diff --git a/src/summary.rs b/src/summary.rs index ee3a8d11..76483d63 100644 --- a/src/summary.rs +++ b/src/summary.rs @@ -183,6 +183,10 @@ impl Assignment { #[derive(Eq, PartialEq, Hash, Ord, PartialOrd, Debug, Clone)] pub enum Expression { + And { + lhs: Box, + rhs: Box, + }, Assert { condition: Condition, error: Box, @@ -209,6 +213,10 @@ pub enum Expression { lhs: Option>, rhs: Box, }, + Or { + lhs: Box, + rhs: Box, + }, String { text: String, }, @@ -221,6 +229,10 @@ impl Expression { fn new(expression: &full::Expression) -> Self { use full::Expression::*; match expression { + And { lhs, rhs } => Self::And { + lhs: Self::new(lhs).into(), + rhs: Self::new(rhs).into(), + }, Assert { condition: full::Condition { lhs, rhs, operator }, error, @@ -250,11 +262,9 @@ impl Expression { .. } => { let mut arguments = Vec::new(); - if let Some(b) = opt_b.as_ref() { arguments.push(Self::new(b)); } - arguments.push(Self::new(a)); Self::Call { name: name.lexeme().to_owned(), @@ -308,10 +318,6 @@ impl Expression { lhs: Self::new(lhs).into(), rhs: Self::new(rhs).into(), }, - Join { lhs, rhs } => Self::Join { - lhs: lhs.as_ref().map(|lhs| Self::new(lhs).into()), - rhs: Self::new(rhs).into(), - }, Conditional { condition: full::Condition { lhs, rhs, operator }, otherwise, @@ -323,13 +329,21 @@ impl Expression { rhs: Self::new(rhs).into(), then: Self::new(then).into(), }, + Group { contents } => Self::new(contents), + Join { lhs, rhs } => Self::Join { + lhs: lhs.as_ref().map(|lhs| Self::new(lhs).into()), + rhs: Self::new(rhs).into(), + }, + Or { lhs, rhs } => Self::Or { + lhs: Self::new(lhs).into(), + rhs: Self::new(rhs).into(), + }, StringLiteral { string_literal } => Self::String { text: string_literal.cooked.clone(), }, Variable { name, .. } => Self::Variable { name: name.lexeme().to_owned(), }, - Group { contents } => Self::new(contents), } } } diff --git a/src/token_kind.rs b/src/token_kind.rs index 0db15d2d..850afa96 100644 --- a/src/token_kind.rs +++ b/src/token_kind.rs @@ -7,6 +7,7 @@ pub(crate) enum TokenKind { At, Backtick, BangEquals, + BarBar, BraceL, BraceR, BracketL, @@ -50,6 +51,7 @@ impl Display for TokenKind { At => "'@'", Backtick => "backtick", BangEquals => "'!='", + BarBar => "'||'", BraceL => "'{'", BraceR => "'}'", BracketL => "'['", diff --git a/src/unstable_feature.rs b/src/unstable_feature.rs index 07d99540..70e26fab 100644 --- a/src/unstable_feature.rs +++ b/src/unstable_feature.rs @@ -3,6 +3,7 @@ use super::*; #[derive(Copy, Clone, Debug, PartialEq, Ord, Eq, PartialOrd)] pub(crate) enum UnstableFeature { FormatSubcommand, + LogicalOperators, ScriptAttribute, ScriptInterpreterSetting, } @@ -11,6 +12,10 @@ impl Display for UnstableFeature { fn fmt(&self, f: &mut Formatter) -> fmt::Result { match self { Self::FormatSubcommand => write!(f, "The `--fmt` command is currently unstable."), + Self::LogicalOperators => write!( + f, + "The logical operators `&&` and `||` are currently unstable." + ), Self::ScriptAttribute => write!(f, "The `[script]` attribute is currently unstable."), Self::ScriptInterpreterSetting => { write!(f, "The `script-interpreter` setting is currently unstable.") diff --git a/src/variables.rs b/src/variables.rs index 57979563..9de1c98c 100644 --- a/src/variables.rs +++ b/src/variables.rs @@ -16,7 +16,24 @@ impl<'expression, 'src> Iterator for Variables<'expression, 'src> { fn next(&mut self) -> Option> { loop { match self.stack.pop()? { - Expression::StringLiteral { .. } | Expression::Backtick { .. } => {} + Expression::And { lhs, rhs } | Expression::Or { lhs, rhs } => { + self.stack.push(lhs); + self.stack.push(rhs); + } + Expression::Assert { + condition: + Condition { + lhs, + rhs, + operator: _, + }, + error, + } => { + self.stack.push(error); + self.stack.push(rhs); + self.stack.push(lhs); + } + Expression::Backtick { .. } | Expression::StringLiteral { .. } => {} Expression::Call { thunk } => match thunk { Thunk::Nullary { .. } => {} Thunk::Unary { arg, .. } => self.stack.push(arg), @@ -56,6 +73,10 @@ impl<'expression, 'src> Iterator for Variables<'expression, 'src> { } } }, + Expression::Concatenation { lhs, rhs } => { + self.stack.push(rhs); + self.stack.push(lhs); + } Expression::Conditional { condition: Condition { @@ -71,10 +92,8 @@ impl<'expression, 'src> Iterator for Variables<'expression, 'src> { self.stack.push(rhs); self.stack.push(lhs); } - Expression::Variable { name, .. } => return Some(name.token), - Expression::Concatenation { lhs, rhs } => { - self.stack.push(rhs); - self.stack.push(lhs); + Expression::Group { contents } => { + self.stack.push(contents); } Expression::Join { lhs, rhs } => { self.stack.push(rhs); @@ -82,22 +101,7 @@ impl<'expression, 'src> Iterator for Variables<'expression, 'src> { self.stack.push(lhs); } } - Expression::Group { contents } => { - self.stack.push(contents); - } - Expression::Assert { - condition: - Condition { - lhs, - rhs, - operator: _, - }, - error, - } => { - self.stack.push(error); - self.stack.push(rhs); - self.stack.push(lhs); - } + Expression::Variable { name, .. } => return Some(name.token), } } } diff --git a/tests/assert_success.rs b/tests/assert_success.rs index bcb364f8..f9202b7f 100644 --- a/tests/assert_success.rs +++ b/tests/assert_success.rs @@ -1,3 +1,4 @@ +#[track_caller] pub(crate) fn assert_success(output: &std::process::Output) { if !output.status.success() { eprintln!("stderr: {}", String::from_utf8_lossy(&output.stderr)); diff --git a/tests/conditional.rs b/tests/conditional.rs index c1739240..4eab2f4d 100644 --- a/tests/conditional.rs +++ b/tests/conditional.rs @@ -136,7 +136,7 @@ test! { ", stdout: "", stderr: " - error: Expected '!=', '==', '=~', '+', or '/', but found identifier + error: Expected '&&', '!=', '||', '==', '=~', '+', or '/', but found identifier ——▶ justfile:1:12 │ 1 │ a := if '' a '' { '' } else { b } diff --git a/tests/ignore_comments.rs b/tests/ignore_comments.rs index c3028a57..3f068227 100644 --- a/tests/ignore_comments.rs +++ b/tests/ignore_comments.rs @@ -125,7 +125,7 @@ fn comments_still_must_be_parsable_when_ignored() { ) .stderr( " - error: Expected '}}', '(', '+', or '/', but found identifier + error: Expected '&&', '||', '}}', '(', '+', or '/', but found identifier ——▶ justfile:4:12 │ 4 │ # {{ foo bar }} diff --git a/tests/lib.rs b/tests/lib.rs index ec71c665..7c85460b 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -73,6 +73,7 @@ mod invocation_directory; mod json; mod line_prefixes; mod list; +mod logical_operators; mod man; mod misc; mod modules; diff --git a/tests/logical_operators.rs b/tests/logical_operators.rs new file mode 100644 index 00000000..5baa0f52 --- /dev/null +++ b/tests/logical_operators.rs @@ -0,0 +1,83 @@ +use super::*; + +#[track_caller] +fn evaluate(expression: &str, expected: &str) { + Test::new() + .justfile(format!("x := {expression}")) + .env("JUST_UNSTABLE", "1") + .args(["--evaluate", "x"]) + .stdout(expected) + .run(); +} + +#[test] +fn logical_operators_are_unstable() { + Test::new() + .justfile("x := 'foo' && 'bar'") + .args(["--evaluate", "x"]) + .stderr_regex(r"error: The logical operators `&&` and `\|\|` are currently unstable. .*") + .status(EXIT_FAILURE) + .run(); + + Test::new() + .justfile("x := 'foo' || 'bar'") + .args(["--evaluate", "x"]) + .stderr_regex(r"error: The logical operators `&&` and `\|\|` are currently unstable. .*") + .status(EXIT_FAILURE) + .run(); +} + +#[test] +fn and_returns_empty_string_if_lhs_is_empty() { + evaluate("'' && 'hello'", ""); +} + +#[test] +fn and_returns_rhs_if_lhs_is_non_empty() { + evaluate("'hello' && 'goodbye'", "goodbye"); +} + +#[test] +fn and_has_lower_precedence_than_plus() { + evaluate("'' && 'goodbye' + 'foo'", ""); + + evaluate("'foo' + 'hello' && 'goodbye'", "goodbye"); + + evaluate("'foo' + '' && 'goodbye'", "goodbye"); + + evaluate("'foo' + 'hello' && 'goodbye' + 'bar'", "goodbyebar"); +} + +#[test] +fn or_returns_rhs_if_lhs_is_empty() { + evaluate("'' || 'hello'", "hello"); +} + +#[test] +fn or_returns_lhs_if_lhs_is_non_empty() { + evaluate("'hello' || 'goodbye'", "hello"); +} + +#[test] +fn or_has_lower_precedence_than_plus() { + evaluate("'' || 'goodbye' + 'foo'", "goodbyefoo"); + + evaluate("'foo' + 'hello' || 'goodbye'", "foohello"); + + evaluate("'foo' + '' || 'goodbye'", "foo"); + + evaluate("'foo' + 'hello' || 'goodbye' + 'bar'", "foohello"); +} + +#[test] +fn and_has_higher_precedence_than_or() { + evaluate("('' && 'foo') || 'bar'", "bar"); + evaluate("'' && 'foo' || 'bar'", "bar"); + evaluate("'a' && 'b' || 'c'", "b"); +} + +#[test] +fn nesting() { + evaluate("'' || '' || '' || '' || 'foo'", "foo"); + evaluate("'foo' && 'foo' && 'foo' && 'foo' && 'bar'", "bar"); +} diff --git a/tests/shell_expansion.rs b/tests/shell_expansion.rs index 67fdc07d..954e2a33 100644 --- a/tests/shell_expansion.rs +++ b/tests/shell_expansion.rs @@ -25,7 +25,7 @@ fn shell_expanded_strings_must_not_have_whitespace() { .status(1) .stderr( " - error: Expected comment, end of file, end of line, '(', '+', or '/', but found string + error: Expected '&&', '||', comment, end of file, end of line, '(', '+', or '/', but found string ——▶ justfile:1:8 │ 1 │ x := x '$JUST_TEST_VARIABLE'