Skip to content

Commit

Permalink
Add && and || operators (#2444)
Browse files Browse the repository at this point in the history
  • Loading branch information
casey authored Nov 1, 2024
1 parent 4c6368e commit 7030e9c
Show file tree
Hide file tree
Showing 20 changed files with 373 additions and 231 deletions.
8 changes: 7 additions & 1 deletion GRAMMAR.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
43 changes: 39 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand All @@ -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
values<sup>master</sup>, 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:
Expand Down Expand Up @@ -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"
Expand Down
5 changes: 3 additions & 2 deletions src/analyzer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down Expand Up @@ -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 {
Expand Down
130 changes: 23 additions & 107 deletions src/assignment_resolver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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}`");
Expand All @@ -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)]
Expand Down
1 change: 1 addition & 0 deletions src/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use super::*;
#[derive(Debug, Clone)]
pub(crate) struct Ast<'src> {
pub(crate) items: Vec<Item<'src>>,
pub(crate) unstable_features: BTreeSet<UnstableFeature>,
pub(crate) warnings: Vec<Warning>,
pub(crate) working_directory: PathBuf,
}
Expand Down
65 changes: 38 additions & 27 deletions src/evaluator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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, .. } => {
Expand All @@ -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 {
Expand Down Expand Up @@ -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)?)
}
Expand All @@ -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}`"),
})
}
}
Expand Down
Loading

0 comments on commit 7030e9c

Please sign in to comment.