Skip to content

Commit

Permalink
Add conditional expressions (#714)
Browse files Browse the repository at this point in the history
Add conditional expressions of the form:

   foo := if lhs == rhs { then } else { otherwise }

`lhs`, `rhs`, `then`, and `otherwise` are all arbitrary expressions, and
can recursively include other conditionals. Conditionals short-circuit,
so the branch not taken isn't evaluated.

It is also possible to test for inequality with `==`.
  • Loading branch information
casey authored Oct 27, 2020
1 parent 3643a0d commit 19f7ad0
Show file tree
Hide file tree
Showing 23 changed files with 764 additions and 95 deletions.
37 changes: 37 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ unicode-width = "0.1.0"
version = "3.1.1"
features = ["termination"]

[dependencies.strum]
version = "0.19.0"
features = ["derive"]

[dev-dependencies]
executable-path = "1.0.0"
pretty_assertions = "0.6.0"
Expand Down
6 changes: 5 additions & 1 deletion GRAMMAR.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,13 @@ export : 'export' assignment
setting : 'set' 'shell' ':=' '[' string (',' string)* ','? ']'
expression : value '+' expression
expression : 'if' condition '{' expression '}' else '{' expression '}'
| value '+' expression
| value
condition : expression '==' expression
| expression '!=' expression
value : NAME '(' sequence? ')'
| STRING
| RAW_STRING
Expand Down
48 changes: 48 additions & 0 deletions README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,54 @@ serve:
./serve {{localhost}} 8080
```

=== Conditional Expressions

`if`/`else` expressions evaluate different branches depending on if two expressions evaluate to the same value:

```make
foo := if "2" == "2" { "Good!" } else { "1984" }

bar:
@echo "{{foo}}"
```

```sh
$ just bar
Good!
```

It is also possible to test for inequality:

```make
foo := if "hello" != "goodbye" { "xyz" } else { "abc" }

bar:
@echo {{foo}}
```

```sh
$ just bar
abc
```

Conditional expressions short-circuit, which means they only evaluate one of
their branches. This can be used to make sure that backtick expressions don't
run when they shouldn't.

```make
foo := if env_var("RELEASE") == "true" { `get-something-from-release-database` } else { "dummy-value" }
```

Conditionals can be used inside of recipes:

```make
bar foo:
echo {{ if foo == "bar" { "hello" } else { "goodbye" } }}
```

Note the space after the final `}`! Without the space, the interpolation will
be prematurely closed.

=== Setting Variables from the Command Line

Variables can be overridden from the command line.
Expand Down
2 changes: 1 addition & 1 deletion justfile
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ sloc:

@lint:
echo Checking for FIXME/TODO...
! grep --color -En 'FIXME|TODO' src/*.rs
! grep --color -Ein 'fixme|todo|xxx|#\[ignore\]' src/*.rs
echo Checking for long lines...
! grep --color -En '.{101}' src/*.rs

Expand Down
12 changes: 12 additions & 0 deletions src/assignment_resolver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,18 @@ impl<'src: 'run, 'run> AssignmentResolver<'src, 'run> {
self.resolve_expression(lhs)?;
self.resolve_expression(rhs)
},
Expression::Conditional {
lhs,
rhs,
then,
otherwise,
..
} => {
self.resolve_expression(lhs)?;
self.resolve_expression(rhs)?;
self.resolve_expression(then)?;
self.resolve_expression(otherwise)
},
Expression::StringLiteral { .. } | Expression::Backtick { .. } => Ok(()),
Expression::Group { contents } => self.resolve_expression(contents),
}
Expand Down
16 changes: 9 additions & 7 deletions src/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,11 @@ pub(crate) use edit_distance::edit_distance;
pub(crate) use libc::EXIT_FAILURE;
pub(crate) use log::{info, warn};
pub(crate) use snafu::{ResultExt, Snafu};
pub(crate) use strum::{Display, EnumString, IntoStaticStr};
pub(crate) use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};

// modules
pub(crate) use crate::{config_error, keyword, setting};
pub(crate) use crate::{config_error, setting};

// functions
pub(crate) use crate::{default::default, empty::empty, load_dotenv::load_dotenv, output::output};
Expand All @@ -47,12 +48,13 @@ pub(crate) use crate::{
dependency::Dependency, enclosure::Enclosure, evaluator::Evaluator, expression::Expression,
fragment::Fragment, function::Function, function_context::FunctionContext,
interrupt_guard::InterruptGuard, interrupt_handler::InterruptHandler, item::Item,
justfile::Justfile, lexer::Lexer, line::Line, list::List, load_error::LoadError, module::Module,
name::Name, output_error::OutputError, parameter::Parameter, parameter_kind::ParameterKind,
parser::Parser, platform::Platform, position::Position, positional::Positional, recipe::Recipe,
recipe_context::RecipeContext, recipe_resolver::RecipeResolver, runtime_error::RuntimeError,
scope::Scope, search::Search, search_config::SearchConfig, search_error::SearchError, set::Set,
setting::Setting, settings::Settings, shebang::Shebang, show_whitespace::ShowWhitespace,
justfile::Justfile, keyword::Keyword, lexer::Lexer, line::Line, list::List,
load_error::LoadError, module::Module, name::Name, output_error::OutputError,
parameter::Parameter, parameter_kind::ParameterKind, parser::Parser, platform::Platform,
position::Position, positional::Positional, recipe::Recipe, recipe_context::RecipeContext,
recipe_resolver::RecipeResolver, runtime_error::RuntimeError, scope::Scope, search::Search,
search_config::SearchConfig, search_error::SearchError, set::Set, setting::Setting,
settings::Settings, shebang::Shebang, show_whitespace::ShowWhitespace,
string_literal::StringLiteral, subcommand::Subcommand, suggestion::Suggestion, table::Table,
thunk::Thunk, token::Token, token_kind::TokenKind, unresolved_dependency::UnresolvedDependency,
unresolved_recipe::UnresolvedRecipe, use_color::UseColor, variables::Variables,
Expand Down
8 changes: 8 additions & 0 deletions src/compilation_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,11 @@ impl Display for CompilationError<'_> {
writeln!(f, "at most {} {}", max, Count("argument", max))?;
}
},
ExpectedKeyword { expected, found } => writeln!(
f,
"Expected keyword `{}` but found identifier `{}`",
expected, found
)?,
ParameterShadowsVariable { parameter } => {
writeln!(
f,
Expand Down Expand Up @@ -198,6 +203,9 @@ impl Display for CompilationError<'_> {
UnknownSetting { setting } => {
writeln!(f, "Unknown setting `{}`", setting)?;
},
UnexpectedCharacter { expected } => {
writeln!(f, "Expected character `{}`", expected)?;
},
UnknownStartOfToken => {
writeln!(f, "Unknown start of token:")?;
},
Expand Down
7 changes: 7 additions & 0 deletions src/compilation_error_kind.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ pub(crate) enum CompilationErrorKind<'src> {
setting: &'src str,
first: usize,
},
ExpectedKeyword {
expected: Keyword,
found: &'src str,
},
ExtraLeadingWhitespace,
FunctionArgumentCountMismatch {
function: &'src str,
Expand Down Expand Up @@ -86,6 +90,9 @@ pub(crate) enum CompilationErrorKind<'src> {
function: &'src str,
},
UnknownStartOfToken,
UnexpectedCharacter {
expected: char,
},
UnknownSetting {
setting: &'src str,
},
Expand Down
16 changes: 16 additions & 0 deletions src/evaluator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,22 @@ impl<'src, 'run> Evaluator<'src, 'run> {
},
Expression::Concatination { lhs, rhs } =>
Ok(self.evaluate_expression(lhs)? + &self.evaluate_expression(rhs)?),
Expression::Conditional {
lhs,
rhs,
then,
otherwise,
inverted,
} => {
let lhs = self.evaluate_expression(lhs)?;
let rhs = self.evaluate_expression(rhs)?;
let condition = if *inverted { lhs != rhs } else { lhs == rhs };
if condition {
self.evaluate_expression(then)
} else {
self.evaluate_expression(otherwise)
}
},
Expression::Group { contents } => self.evaluate_expression(contents),
}
}
Expand Down
23 changes: 23 additions & 0 deletions src/expression.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ pub(crate) enum Expression<'src> {
lhs: Box<Expression<'src>>,
rhs: Box<Expression<'src>>,
},
/// `if lhs == rhs { then } else { otherwise }`
Conditional {
lhs: Box<Expression<'src>>,
rhs: Box<Expression<'src>>,
then: Box<Expression<'src>>,
otherwise: Box<Expression<'src>>,
inverted: bool,
},
/// `(contents)`
Group { contents: Box<Expression<'src>> },
/// `"string_literal"` or `'string_literal'`
Expand All @@ -39,6 +47,21 @@ impl<'src> Display for Expression<'src> {
match self {
Expression::Backtick { contents, .. } => write!(f, "`{}`", contents),
Expression::Concatination { lhs, rhs } => write!(f, "{} + {}", lhs, rhs),
Expression::Conditional {
lhs,
rhs,
then,
otherwise,
inverted,
} => write!(
f,
"if {} {} {} {{ {} }} else {{ {} }} ",
lhs,
if *inverted { "!=" } else { "==" },
rhs,
then,
otherwise
),
Expression::StringLiteral { string_literal } => write!(f, "{}", string_literal),
Expression::Variable { name } => write!(f, "{}", name.lexeme()),
Expression::Call { thunk } => write!(f, "{}", thunk),
Expand Down
31 changes: 27 additions & 4 deletions src/keyword.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,28 @@
pub(crate) const ALIAS: &str = "alias";
pub(crate) const EXPORT: &str = "export";
pub(crate) const SET: &str = "set";
use crate::common::*;

pub(crate) const SHELL: &str = "shell";
#[derive(Debug, Eq, PartialEq, IntoStaticStr, Display, Copy, Clone, EnumString)]
#[strum(serialize_all = "kebab_case")]
pub(crate) enum Keyword {
Alias,
Else,
Export,
If,
Set,
Shell,
}

impl Keyword {
pub(crate) fn from_lexeme(lexeme: &str) -> Option<Keyword> {
lexeme.parse().ok()
}

pub(crate) fn lexeme(self) -> &'static str {
self.into()
}
}

impl<'a> PartialEq<&'a str> for Keyword {
fn eq(&self, other: &&'a str) -> bool {
self.lexeme() == *other
}
}
Loading

0 comments on commit 19f7ad0

Please sign in to comment.