Skip to content

Commit

Permalink
Add && and || operators
Browse files Browse the repository at this point in the history
  • Loading branch information
casey committed Oct 31, 2024
1 parent 528c9f0 commit ad0f4d3
Show file tree
Hide file tree
Showing 17 changed files with 300 additions and 199 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
37 changes: 33 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 `{{…}}` substitutions
inside recipe bodies:

```just
tmpdir := `mktemp -d`
Expand All @@ -1310,6 +1312,33 @@ publish:
rm -rf {{tarball}} {{tardir}}
```

#### Concatenation

The `+` operator returns the left-hand argument concatenated with the
right-hand argument:

```just
foobar := 'foo' + 'bar'
```

#### `&&` and/or `||`

The `&&` operator returns the empty string if the left-hand argument is the
empty string, otherwise it returns the right hand argument:

```just
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:

```just
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 +2396,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
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
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(lhs);
}
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 ad0f4d3

Please sign in to comment.