Add conditional expressions (#714)
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 `==`.
This commit is contained in:
parent
3643a0dff0
commit
19f7ad09a7
37
Cargo.lock
generated
37
Cargo.lock
generated
@ -156,6 +156,15 @@ dependencies = [
|
|||||||
"wasi",
|
"wasi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "heck"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-segmentation",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hermit-abi"
|
name = "hermit-abi"
|
||||||
version = "0.1.17"
|
version = "0.1.17"
|
||||||
@ -192,6 +201,7 @@ dependencies = [
|
|||||||
"log",
|
"log",
|
||||||
"pretty_assertions",
|
"pretty_assertions",
|
||||||
"snafu",
|
"snafu",
|
||||||
|
"strum",
|
||||||
"target",
|
"target",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"test-utilities",
|
"test-utilities",
|
||||||
@ -390,6 +400,27 @@ version = "0.8.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
|
checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strum"
|
||||||
|
version = "0.19.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b89a286a7e3b5720b9a477b23253bc50debac207c8d21505f8e70b36792f11b5"
|
||||||
|
dependencies = [
|
||||||
|
"strum_macros",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strum_macros"
|
||||||
|
version = "0.19.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e61bb0be289045cb80bfce000512e32d09f8337e54c186725da381377ad1f8d5"
|
||||||
|
dependencies = [
|
||||||
|
"heck",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "1.0.44"
|
version = "1.0.44"
|
||||||
@ -475,6 +506,12 @@ dependencies = [
|
|||||||
"lazy_static",
|
"lazy_static",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-segmentation"
|
||||||
|
version = "1.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-width"
|
name = "unicode-width"
|
||||||
version = "0.1.8"
|
version = "0.1.8"
|
||||||
|
@ -30,6 +30,10 @@ unicode-width = "0.1.0"
|
|||||||
version = "3.1.1"
|
version = "3.1.1"
|
||||||
features = ["termination"]
|
features = ["termination"]
|
||||||
|
|
||||||
|
[dependencies.strum]
|
||||||
|
version = "0.19.0"
|
||||||
|
features = ["derive"]
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
executable-path = "1.0.0"
|
executable-path = "1.0.0"
|
||||||
pretty_assertions = "0.6.0"
|
pretty_assertions = "0.6.0"
|
||||||
|
@ -57,9 +57,13 @@ export : 'export' assignment
|
|||||||
|
|
||||||
setting : 'set' 'shell' ':=' '[' string (',' string)* ','? ']'
|
setting : 'set' 'shell' ':=' '[' string (',' string)* ','? ']'
|
||||||
|
|
||||||
expression : value '+' expression
|
expression : 'if' condition '{' expression '}' else '{' expression '}'
|
||||||
|
| value '+' expression
|
||||||
| value
|
| value
|
||||||
|
|
||||||
|
condition : expression '==' expression
|
||||||
|
| expression '!=' expression
|
||||||
|
|
||||||
value : NAME '(' sequence? ')'
|
value : NAME '(' sequence? ')'
|
||||||
| STRING
|
| STRING
|
||||||
| RAW_STRING
|
| RAW_STRING
|
||||||
|
48
README.adoc
48
README.adoc
@ -545,6 +545,54 @@ serve:
|
|||||||
./serve {{localhost}} 8080
|
./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
|
=== Setting Variables from the Command Line
|
||||||
|
|
||||||
Variables can be overridden from the command line.
|
Variables can be overridden from the command line.
|
||||||
|
2
justfile
2
justfile
@ -121,7 +121,7 @@ sloc:
|
|||||||
|
|
||||||
@lint:
|
@lint:
|
||||||
echo Checking for FIXME/TODO...
|
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...
|
echo Checking for long lines...
|
||||||
! grep --color -En '.{101}' src/*.rs
|
! grep --color -En '.{101}' src/*.rs
|
||||||
|
|
||||||
|
@ -87,6 +87,18 @@ impl<'src: 'run, 'run> AssignmentResolver<'src, 'run> {
|
|||||||
self.resolve_expression(lhs)?;
|
self.resolve_expression(lhs)?;
|
||||||
self.resolve_expression(rhs)
|
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::StringLiteral { .. } | Expression::Backtick { .. } => Ok(()),
|
||||||
Expression::Group { contents } => self.resolve_expression(contents),
|
Expression::Group { contents } => self.resolve_expression(contents),
|
||||||
}
|
}
|
||||||
|
@ -24,10 +24,11 @@ pub(crate) use edit_distance::edit_distance;
|
|||||||
pub(crate) use libc::EXIT_FAILURE;
|
pub(crate) use libc::EXIT_FAILURE;
|
||||||
pub(crate) use log::{info, warn};
|
pub(crate) use log::{info, warn};
|
||||||
pub(crate) use snafu::{ResultExt, Snafu};
|
pub(crate) use snafu::{ResultExt, Snafu};
|
||||||
|
pub(crate) use strum::{Display, EnumString, IntoStaticStr};
|
||||||
pub(crate) use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
|
pub(crate) use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
|
||||||
|
|
||||||
// modules
|
// modules
|
||||||
pub(crate) use crate::{config_error, keyword, setting};
|
pub(crate) use crate::{config_error, setting};
|
||||||
|
|
||||||
// functions
|
// functions
|
||||||
pub(crate) use crate::{default::default, empty::empty, load_dotenv::load_dotenv, output::output};
|
pub(crate) use crate::{default::default, empty::empty, load_dotenv::load_dotenv, output::output};
|
||||||
@ -47,12 +48,13 @@ pub(crate) use crate::{
|
|||||||
dependency::Dependency, enclosure::Enclosure, evaluator::Evaluator, expression::Expression,
|
dependency::Dependency, enclosure::Enclosure, evaluator::Evaluator, expression::Expression,
|
||||||
fragment::Fragment, function::Function, function_context::FunctionContext,
|
fragment::Fragment, function::Function, function_context::FunctionContext,
|
||||||
interrupt_guard::InterruptGuard, interrupt_handler::InterruptHandler, item::Item,
|
interrupt_guard::InterruptGuard, interrupt_handler::InterruptHandler, item::Item,
|
||||||
justfile::Justfile, lexer::Lexer, line::Line, list::List, load_error::LoadError, module::Module,
|
justfile::Justfile, keyword::Keyword, lexer::Lexer, line::Line, list::List,
|
||||||
name::Name, output_error::OutputError, parameter::Parameter, parameter_kind::ParameterKind,
|
load_error::LoadError, module::Module, name::Name, output_error::OutputError,
|
||||||
parser::Parser, platform::Platform, position::Position, positional::Positional, recipe::Recipe,
|
parameter::Parameter, parameter_kind::ParameterKind, parser::Parser, platform::Platform,
|
||||||
recipe_context::RecipeContext, recipe_resolver::RecipeResolver, runtime_error::RuntimeError,
|
position::Position, positional::Positional, recipe::Recipe, recipe_context::RecipeContext,
|
||||||
scope::Scope, search::Search, search_config::SearchConfig, search_error::SearchError, set::Set,
|
recipe_resolver::RecipeResolver, runtime_error::RuntimeError, scope::Scope, search::Search,
|
||||||
setting::Setting, settings::Settings, shebang::Shebang, show_whitespace::ShowWhitespace,
|
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,
|
string_literal::StringLiteral, subcommand::Subcommand, suggestion::Suggestion, table::Table,
|
||||||
thunk::Thunk, token::Token, token_kind::TokenKind, unresolved_dependency::UnresolvedDependency,
|
thunk::Thunk, token::Token, token_kind::TokenKind, unresolved_dependency::UnresolvedDependency,
|
||||||
unresolved_recipe::UnresolvedRecipe, use_color::UseColor, variables::Variables,
|
unresolved_recipe::UnresolvedRecipe, use_color::UseColor, variables::Variables,
|
||||||
|
@ -128,6 +128,11 @@ impl Display for CompilationError<'_> {
|
|||||||
writeln!(f, "at most {} {}", max, Count("argument", max))?;
|
writeln!(f, "at most {} {}", max, Count("argument", max))?;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
ExpectedKeyword { expected, found } => writeln!(
|
||||||
|
f,
|
||||||
|
"Expected keyword `{}` but found identifier `{}`",
|
||||||
|
expected, found
|
||||||
|
)?,
|
||||||
ParameterShadowsVariable { parameter } => {
|
ParameterShadowsVariable { parameter } => {
|
||||||
writeln!(
|
writeln!(
|
||||||
f,
|
f,
|
||||||
@ -198,6 +203,9 @@ impl Display for CompilationError<'_> {
|
|||||||
UnknownSetting { setting } => {
|
UnknownSetting { setting } => {
|
||||||
writeln!(f, "Unknown setting `{}`", setting)?;
|
writeln!(f, "Unknown setting `{}`", setting)?;
|
||||||
},
|
},
|
||||||
|
UnexpectedCharacter { expected } => {
|
||||||
|
writeln!(f, "Expected character `{}`", expected)?;
|
||||||
|
},
|
||||||
UnknownStartOfToken => {
|
UnknownStartOfToken => {
|
||||||
writeln!(f, "Unknown start of token:")?;
|
writeln!(f, "Unknown start of token:")?;
|
||||||
},
|
},
|
||||||
|
@ -39,6 +39,10 @@ pub(crate) enum CompilationErrorKind<'src> {
|
|||||||
setting: &'src str,
|
setting: &'src str,
|
||||||
first: usize,
|
first: usize,
|
||||||
},
|
},
|
||||||
|
ExpectedKeyword {
|
||||||
|
expected: Keyword,
|
||||||
|
found: &'src str,
|
||||||
|
},
|
||||||
ExtraLeadingWhitespace,
|
ExtraLeadingWhitespace,
|
||||||
FunctionArgumentCountMismatch {
|
FunctionArgumentCountMismatch {
|
||||||
function: &'src str,
|
function: &'src str,
|
||||||
@ -86,6 +90,9 @@ pub(crate) enum CompilationErrorKind<'src> {
|
|||||||
function: &'src str,
|
function: &'src str,
|
||||||
},
|
},
|
||||||
UnknownStartOfToken,
|
UnknownStartOfToken,
|
||||||
|
UnexpectedCharacter {
|
||||||
|
expected: char,
|
||||||
|
},
|
||||||
UnknownSetting {
|
UnknownSetting {
|
||||||
setting: &'src str,
|
setting: &'src str,
|
||||||
},
|
},
|
||||||
|
@ -116,6 +116,22 @@ impl<'src, 'run> Evaluator<'src, 'run> {
|
|||||||
},
|
},
|
||||||
Expression::Concatination { lhs, rhs } =>
|
Expression::Concatination { lhs, rhs } =>
|
||||||
Ok(self.evaluate_expression(lhs)? + &self.evaluate_expression(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),
|
Expression::Group { contents } => self.evaluate_expression(contents),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,14 @@ pub(crate) enum Expression<'src> {
|
|||||||
lhs: Box<Expression<'src>>,
|
lhs: Box<Expression<'src>>,
|
||||||
rhs: 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)`
|
/// `(contents)`
|
||||||
Group { contents: Box<Expression<'src>> },
|
Group { contents: Box<Expression<'src>> },
|
||||||
/// `"string_literal"` or `'string_literal'`
|
/// `"string_literal"` or `'string_literal'`
|
||||||
@ -39,6 +47,21 @@ impl<'src> Display for Expression<'src> {
|
|||||||
match self {
|
match self {
|
||||||
Expression::Backtick { contents, .. } => write!(f, "`{}`", contents),
|
Expression::Backtick { contents, .. } => write!(f, "`{}`", contents),
|
||||||
Expression::Concatination { lhs, rhs } => write!(f, "{} + {}", lhs, rhs),
|
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::StringLiteral { string_literal } => write!(f, "{}", string_literal),
|
||||||
Expression::Variable { name } => write!(f, "{}", name.lexeme()),
|
Expression::Variable { name } => write!(f, "{}", name.lexeme()),
|
||||||
Expression::Call { thunk } => write!(f, "{}", thunk),
|
Expression::Call { thunk } => write!(f, "{}", thunk),
|
||||||
|
@ -1,5 +1,28 @@
|
|||||||
pub(crate) const ALIAS: &str = "alias";
|
use crate::common::*;
|
||||||
pub(crate) const EXPORT: &str = "export";
|
|
||||||
pub(crate) const SET: &str = "set";
|
|
||||||
|
|
||||||
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
218
src/lexer.rs
218
src/lexer.rs
@ -98,6 +98,25 @@ impl<'src> Lexer<'src> {
|
|||||||
self.token_end.offset - self.token_start.offset
|
self.token_end.offset - self.token_start.offset
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn accepted(&mut self, c: char) -> CompilationResult<'src, bool> {
|
||||||
|
if self.next_is(c) {
|
||||||
|
self.advance()?;
|
||||||
|
Ok(true)
|
||||||
|
} else {
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn presume(&mut self, c: char) -> CompilationResult<'src, ()> {
|
||||||
|
if !self.next_is(c) {
|
||||||
|
return Err(self.internal_error(format!("Lexer presumed character `{}`", c)));
|
||||||
|
}
|
||||||
|
|
||||||
|
self.advance()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Is next character c?
|
/// Is next character c?
|
||||||
fn next_is(&self, c: char) -> bool {
|
fn next_is(&self, c: char) -> bool {
|
||||||
self.next == Some(c)
|
self.next == Some(c)
|
||||||
@ -430,17 +449,18 @@ impl<'src> Lexer<'src> {
|
|||||||
/// Lex token beginning with `start` outside of a recipe body
|
/// Lex token beginning with `start` outside of a recipe body
|
||||||
fn lex_normal(&mut self, start: char) -> CompilationResult<'src, ()> {
|
fn lex_normal(&mut self, start: char) -> CompilationResult<'src, ()> {
|
||||||
match start {
|
match start {
|
||||||
|
'!' => self.lex_bang(),
|
||||||
'*' => self.lex_single(Asterisk),
|
'*' => self.lex_single(Asterisk),
|
||||||
'@' => self.lex_single(At),
|
'@' => self.lex_single(At),
|
||||||
'[' => self.lex_single(BracketL),
|
'[' => self.lex_single(BracketL),
|
||||||
']' => self.lex_single(BracketR),
|
']' => self.lex_single(BracketR),
|
||||||
'=' => self.lex_single(Equals),
|
'=' => self.lex_choice('=', EqualsEquals, Equals),
|
||||||
',' => self.lex_single(Comma),
|
',' => self.lex_single(Comma),
|
||||||
':' => self.lex_colon(),
|
':' => self.lex_colon(),
|
||||||
'(' => self.lex_single(ParenL),
|
'(' => self.lex_single(ParenL),
|
||||||
')' => self.lex_single(ParenR),
|
')' => self.lex_single(ParenR),
|
||||||
'{' => self.lex_brace_l(),
|
'{' => self.lex_single(BraceL),
|
||||||
'}' => self.lex_brace_r(),
|
'}' => self.lex_single(BraceR),
|
||||||
'+' => self.lex_single(Plus),
|
'+' => self.lex_single(Plus),
|
||||||
'\n' => self.lex_single(Eol),
|
'\n' => self.lex_single(Eol),
|
||||||
'\r' => self.lex_cr_lf(),
|
'\r' => self.lex_cr_lf(),
|
||||||
@ -449,13 +469,11 @@ impl<'src> Lexer<'src> {
|
|||||||
' ' | '\t' => self.lex_whitespace(),
|
' ' | '\t' => self.lex_whitespace(),
|
||||||
'\'' => self.lex_raw_string(),
|
'\'' => self.lex_raw_string(),
|
||||||
'"' => self.lex_cooked_string(),
|
'"' => self.lex_cooked_string(),
|
||||||
_ =>
|
_ if Self::is_identifier_start(start) => self.lex_identifier(),
|
||||||
if Self::is_identifier_start(start) {
|
_ => {
|
||||||
self.lex_identifier()
|
self.advance()?;
|
||||||
} else {
|
Err(self.error(UnknownStartOfToken))
|
||||||
self.advance()?;
|
},
|
||||||
Err(self.error(UnknownStartOfToken))
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -465,7 +483,6 @@ impl<'src> Lexer<'src> {
|
|||||||
interpolation_start: Token<'src>,
|
interpolation_start: Token<'src>,
|
||||||
start: char,
|
start: char,
|
||||||
) -> CompilationResult<'src, ()> {
|
) -> CompilationResult<'src, ()> {
|
||||||
// Check for end of interpolation
|
|
||||||
if self.rest_starts_with("}}") {
|
if self.rest_starts_with("}}") {
|
||||||
// end current interpolation
|
// end current interpolation
|
||||||
self.interpolation_start = None;
|
self.interpolation_start = None;
|
||||||
@ -537,14 +554,14 @@ impl<'src> Lexer<'src> {
|
|||||||
self.recipe_body = false;
|
self.recipe_body = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Lex a single character token
|
/// Lex a single-character token
|
||||||
fn lex_single(&mut self, kind: TokenKind) -> CompilationResult<'src, ()> {
|
fn lex_single(&mut self, kind: TokenKind) -> CompilationResult<'src, ()> {
|
||||||
self.advance()?;
|
self.advance()?;
|
||||||
self.token(kind);
|
self.token(kind);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Lex a double character token
|
/// Lex a double-character token
|
||||||
fn lex_double(&mut self, kind: TokenKind) -> CompilationResult<'src, ()> {
|
fn lex_double(&mut self, kind: TokenKind) -> CompilationResult<'src, ()> {
|
||||||
self.advance()?;
|
self.advance()?;
|
||||||
self.advance()?;
|
self.advance()?;
|
||||||
@ -552,12 +569,48 @@ impl<'src> Lexer<'src> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Lex a token starting with ':'
|
/// Lex a double-character token of kind `then` if the second character of
|
||||||
fn lex_colon(&mut self) -> CompilationResult<'src, ()> {
|
/// that token would be `second`, otherwise lex a single-character token of
|
||||||
|
/// kind `otherwise`
|
||||||
|
fn lex_choice(
|
||||||
|
&mut self,
|
||||||
|
second: char,
|
||||||
|
then: TokenKind,
|
||||||
|
otherwise: TokenKind,
|
||||||
|
) -> CompilationResult<'src, ()> {
|
||||||
self.advance()?;
|
self.advance()?;
|
||||||
|
|
||||||
if self.next_is('=') {
|
if self.accepted(second)? {
|
||||||
|
self.token(then);
|
||||||
|
} else {
|
||||||
|
self.token(otherwise);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lex a token starting with '!'
|
||||||
|
fn lex_bang(&mut self) -> CompilationResult<'src, ()> {
|
||||||
|
self.presume('!')?;
|
||||||
|
|
||||||
|
if self.accepted('=')? {
|
||||||
|
self.token(BangEquals);
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
// Emit an unspecified token to consume the current character,
|
||||||
|
self.token(Unspecified);
|
||||||
|
// …and advance past another character,
|
||||||
self.advance()?;
|
self.advance()?;
|
||||||
|
// …so that the error we produce highlights the unexpected character.
|
||||||
|
Err(self.error(UnexpectedCharacter { expected: '=' }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lex a token starting with ':'
|
||||||
|
fn lex_colon(&mut self) -> CompilationResult<'src, ()> {
|
||||||
|
self.presume(':')?;
|
||||||
|
|
||||||
|
if self.accepted('=')? {
|
||||||
self.token(ColonEquals);
|
self.token(ColonEquals);
|
||||||
} else {
|
} else {
|
||||||
self.token(Colon);
|
self.token(Colon);
|
||||||
@ -567,43 +620,21 @@ impl<'src> Lexer<'src> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Lex a token starting with '{'
|
|
||||||
fn lex_brace_l(&mut self) -> CompilationResult<'src, ()> {
|
|
||||||
if !self.rest_starts_with("{{") {
|
|
||||||
self.advance()?;
|
|
||||||
|
|
||||||
return Err(self.error(UnknownStartOfToken));
|
|
||||||
}
|
|
||||||
|
|
||||||
self.lex_double(InterpolationStart)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Lex a token starting with '}'
|
|
||||||
fn lex_brace_r(&mut self) -> CompilationResult<'src, ()> {
|
|
||||||
if !self.rest_starts_with("}}") {
|
|
||||||
self.advance()?;
|
|
||||||
|
|
||||||
return Err(self.error(UnknownStartOfToken));
|
|
||||||
}
|
|
||||||
|
|
||||||
self.lex_double(InterpolationEnd)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Lex a carriage return and line feed
|
/// Lex a carriage return and line feed
|
||||||
fn lex_cr_lf(&mut self) -> CompilationResult<'src, ()> {
|
fn lex_cr_lf(&mut self) -> CompilationResult<'src, ()> {
|
||||||
if !self.rest_starts_with("\r\n") {
|
self.presume('\r')?;
|
||||||
// advance over \r
|
|
||||||
self.advance()?;
|
|
||||||
|
|
||||||
|
if !self.accepted('\n')? {
|
||||||
return Err(self.error(UnpairedCarriageReturn));
|
return Err(self.error(UnpairedCarriageReturn));
|
||||||
}
|
}
|
||||||
|
|
||||||
self.lex_double(Eol)
|
self.token(Eol);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Lex name: [a-zA-Z_][a-zA-Z0-9_]*
|
/// Lex name: [a-zA-Z_][a-zA-Z0-9_]*
|
||||||
fn lex_identifier(&mut self) -> CompilationResult<'src, ()> {
|
fn lex_identifier(&mut self) -> CompilationResult<'src, ()> {
|
||||||
// advance over initial character
|
|
||||||
self.advance()?;
|
self.advance()?;
|
||||||
|
|
||||||
while let Some(c) = self.next {
|
while let Some(c) = self.next {
|
||||||
@ -621,8 +652,7 @@ impl<'src> Lexer<'src> {
|
|||||||
|
|
||||||
/// Lex comment: #[^\r\n]
|
/// Lex comment: #[^\r\n]
|
||||||
fn lex_comment(&mut self) -> CompilationResult<'src, ()> {
|
fn lex_comment(&mut self) -> CompilationResult<'src, ()> {
|
||||||
// advance over #
|
self.presume('#')?;
|
||||||
self.advance()?;
|
|
||||||
|
|
||||||
while !self.at_eol_or_eof() {
|
while !self.at_eol_or_eof() {
|
||||||
self.advance()?;
|
self.advance()?;
|
||||||
@ -665,8 +695,7 @@ impl<'src> Lexer<'src> {
|
|||||||
|
|
||||||
/// Lex raw string: '[^']*'
|
/// Lex raw string: '[^']*'
|
||||||
fn lex_raw_string(&mut self) -> CompilationResult<'src, ()> {
|
fn lex_raw_string(&mut self) -> CompilationResult<'src, ()> {
|
||||||
// advance over opening '
|
self.presume('\'')?;
|
||||||
self.advance()?;
|
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
match self.next {
|
match self.next {
|
||||||
@ -678,8 +707,7 @@ impl<'src> Lexer<'src> {
|
|||||||
self.advance()?;
|
self.advance()?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// advance over closing '
|
self.presume('\'')?;
|
||||||
self.advance()?;
|
|
||||||
|
|
||||||
self.token(StringRaw);
|
self.token(StringRaw);
|
||||||
|
|
||||||
@ -688,8 +716,7 @@ impl<'src> Lexer<'src> {
|
|||||||
|
|
||||||
/// Lex cooked string: "[^"\n\r]*" (also processes escape sequences)
|
/// Lex cooked string: "[^"\n\r]*" (also processes escape sequences)
|
||||||
fn lex_cooked_string(&mut self) -> CompilationResult<'src, ()> {
|
fn lex_cooked_string(&mut self) -> CompilationResult<'src, ()> {
|
||||||
// advance over opening "
|
self.presume('"')?;
|
||||||
self.advance()?;
|
|
||||||
|
|
||||||
let mut escape = false;
|
let mut escape = false;
|
||||||
|
|
||||||
@ -803,6 +830,9 @@ mod tests {
|
|||||||
// Fixed lexemes
|
// Fixed lexemes
|
||||||
Asterisk => "*",
|
Asterisk => "*",
|
||||||
At => "@",
|
At => "@",
|
||||||
|
BangEquals => "!=",
|
||||||
|
BraceL => "{",
|
||||||
|
BraceR => "}",
|
||||||
BracketL => "[",
|
BracketL => "[",
|
||||||
BracketR => "]",
|
BracketR => "]",
|
||||||
Colon => ":",
|
Colon => ":",
|
||||||
@ -810,6 +840,7 @@ mod tests {
|
|||||||
Comma => ",",
|
Comma => ",",
|
||||||
Eol => "\n",
|
Eol => "\n",
|
||||||
Equals => "=",
|
Equals => "=",
|
||||||
|
EqualsEquals => "==",
|
||||||
Indent => " ",
|
Indent => " ",
|
||||||
InterpolationEnd => "}}",
|
InterpolationEnd => "}}",
|
||||||
InterpolationStart => "{{",
|
InterpolationStart => "{{",
|
||||||
@ -901,6 +932,48 @@ mod tests {
|
|||||||
tokens: (StringCooked:"\"hello\""),
|
tokens: (StringCooked:"\"hello\""),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: equals,
|
||||||
|
text: "=",
|
||||||
|
tokens: (Equals),
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: equals_equals,
|
||||||
|
text: "==",
|
||||||
|
tokens: (EqualsEquals),
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: bang_equals,
|
||||||
|
text: "!=",
|
||||||
|
tokens: (BangEquals),
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: brace_l,
|
||||||
|
text: "{",
|
||||||
|
tokens: (BraceL),
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: brace_r,
|
||||||
|
text: "}",
|
||||||
|
tokens: (BraceR),
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: brace_lll,
|
||||||
|
text: "{{{",
|
||||||
|
tokens: (BraceL, BraceL, BraceL),
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: brace_rrr,
|
||||||
|
text: "}}}",
|
||||||
|
tokens: (BraceR, BraceR, BraceR),
|
||||||
|
}
|
||||||
|
|
||||||
test! {
|
test! {
|
||||||
name: export_concatination,
|
name: export_concatination,
|
||||||
text: "export foo = 'foo' + 'bar'",
|
text: "export foo = 'foo' + 'bar'",
|
||||||
@ -1965,4 +2038,47 @@ mod tests {
|
|||||||
width: 2,
|
width: 2,
|
||||||
kind: UnterminatedInterpolation,
|
kind: UnterminatedInterpolation,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
error! {
|
||||||
|
name: unexpected_character_after_bang,
|
||||||
|
input: "!{",
|
||||||
|
offset: 1,
|
||||||
|
line: 0,
|
||||||
|
column: 1,
|
||||||
|
width: 1,
|
||||||
|
kind: UnexpectedCharacter { expected: '=' },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn presume_error() {
|
||||||
|
assert_matches!(
|
||||||
|
Lexer::new("!").presume('-').unwrap_err(),
|
||||||
|
CompilationError {
|
||||||
|
token: Token {
|
||||||
|
offset: 0,
|
||||||
|
line: 0,
|
||||||
|
column: 0,
|
||||||
|
length: 0,
|
||||||
|
src: "!",
|
||||||
|
kind: Unspecified,
|
||||||
|
},
|
||||||
|
kind: Internal {
|
||||||
|
message,
|
||||||
|
},
|
||||||
|
} if message == "Lexer presumed character `-`"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
Lexer::new("!").presume('-').unwrap_err().to_string(),
|
||||||
|
testing::unindent(
|
||||||
|
"
|
||||||
|
Internal error, this may indicate a bug in just: Lexer presumed character `-`
|
||||||
|
\
|
||||||
|
consider filing an issue: https://github.com/casey/just/issues/new
|
||||||
|
|
|
||||||
|
1 | !
|
||||||
|
| ^"
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
27
src/node.rs
27
src/node.rs
@ -28,7 +28,7 @@ impl<'src> Node<'src> for Item<'src> {
|
|||||||
|
|
||||||
impl<'src> Node<'src> for Alias<'src, Name<'src>> {
|
impl<'src> Node<'src> for Alias<'src, Name<'src>> {
|
||||||
fn tree(&self) -> Tree<'src> {
|
fn tree(&self) -> Tree<'src> {
|
||||||
Tree::atom(keyword::ALIAS)
|
Tree::atom(Keyword::Alias.lexeme())
|
||||||
.push(self.name.lexeme())
|
.push(self.name.lexeme())
|
||||||
.push(self.target.lexeme())
|
.push(self.target.lexeme())
|
||||||
}
|
}
|
||||||
@ -37,7 +37,9 @@ impl<'src> Node<'src> for Alias<'src, Name<'src>> {
|
|||||||
impl<'src> Node<'src> for Assignment<'src> {
|
impl<'src> Node<'src> for Assignment<'src> {
|
||||||
fn tree(&self) -> Tree<'src> {
|
fn tree(&self) -> Tree<'src> {
|
||||||
if self.export {
|
if self.export {
|
||||||
Tree::atom("assignment").push("#").push(keyword::EXPORT)
|
Tree::atom("assignment")
|
||||||
|
.push("#")
|
||||||
|
.push(Keyword::Export.lexeme())
|
||||||
} else {
|
} else {
|
||||||
Tree::atom("assignment")
|
Tree::atom("assignment")
|
||||||
}
|
}
|
||||||
@ -50,6 +52,25 @@ impl<'src> Node<'src> for Expression<'src> {
|
|||||||
fn tree(&self) -> Tree<'src> {
|
fn tree(&self) -> Tree<'src> {
|
||||||
match self {
|
match self {
|
||||||
Expression::Concatination { lhs, rhs } => Tree::atom("+").push(lhs.tree()).push(rhs.tree()),
|
Expression::Concatination { lhs, rhs } => Tree::atom("+").push(lhs.tree()).push(rhs.tree()),
|
||||||
|
Expression::Conditional {
|
||||||
|
lhs,
|
||||||
|
rhs,
|
||||||
|
then,
|
||||||
|
otherwise,
|
||||||
|
inverted,
|
||||||
|
} => {
|
||||||
|
let mut tree = Tree::atom(Keyword::If.lexeme());
|
||||||
|
tree.push_mut(lhs.tree());
|
||||||
|
if *inverted {
|
||||||
|
tree.push_mut("!=")
|
||||||
|
} else {
|
||||||
|
tree.push_mut("==")
|
||||||
|
}
|
||||||
|
tree.push_mut(rhs.tree());
|
||||||
|
tree.push_mut(then.tree());
|
||||||
|
tree.push_mut(otherwise.tree());
|
||||||
|
tree
|
||||||
|
},
|
||||||
Expression::Call { thunk } => {
|
Expression::Call { thunk } => {
|
||||||
let mut tree = Tree::atom("call");
|
let mut tree = Tree::atom("call");
|
||||||
|
|
||||||
@ -164,7 +185,7 @@ impl<'src> Node<'src> for Fragment<'src> {
|
|||||||
|
|
||||||
impl<'src> Node<'src> for Set<'src> {
|
impl<'src> Node<'src> for Set<'src> {
|
||||||
fn tree(&self) -> Tree<'src> {
|
fn tree(&self) -> Tree<'src> {
|
||||||
let mut set = Tree::atom(keyword::SET);
|
let mut set = Tree::atom(Keyword::Set.lexeme());
|
||||||
|
|
||||||
set.push_mut(self.name.lexeme());
|
set.push_mut(self.name.lexeme());
|
||||||
|
|
||||||
|
163
src/parser.rs
163
src/parser.rs
@ -172,9 +172,20 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
|
|||||||
self.expect(Eol).map(|_| ())
|
self.expect(Eol).map(|_| ())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn expect_keyword(&mut self, expected: Keyword) -> CompilationResult<'src, ()> {
|
||||||
|
let identifier = self.expect(Identifier)?;
|
||||||
|
let found = identifier.lexeme();
|
||||||
|
|
||||||
|
if expected == found {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(identifier.error(CompilationErrorKind::ExpectedKeyword { expected, found }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Return an internal error if the next token is not of kind `Identifier`
|
/// Return an internal error if the next token is not of kind `Identifier`
|
||||||
/// with lexeme `lexeme`.
|
/// with lexeme `lexeme`.
|
||||||
fn presume_name(&mut self, lexeme: &str) -> CompilationResult<'src, ()> {
|
fn presume_keyword(&mut self, keyword: Keyword) -> CompilationResult<'src, ()> {
|
||||||
let next = self.advance()?;
|
let next = self.advance()?;
|
||||||
|
|
||||||
if next.kind != Identifier {
|
if next.kind != Identifier {
|
||||||
@ -182,10 +193,10 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
|
|||||||
"Presumed next token would have kind {}, but found {}",
|
"Presumed next token would have kind {}, but found {}",
|
||||||
Identifier, next.kind
|
Identifier, next.kind
|
||||||
))?)
|
))?)
|
||||||
} else if next.lexeme() != lexeme {
|
} else if keyword != next.lexeme() {
|
||||||
Err(self.internal_error(format!(
|
Err(self.internal_error(format!(
|
||||||
"Presumed next token would have lexeme \"{}\", but found \"{}\"",
|
"Presumed next token would have lexeme \"{}\", but found \"{}\"",
|
||||||
lexeme,
|
keyword,
|
||||||
next.lexeme(),
|
next.lexeme(),
|
||||||
))?)
|
))?)
|
||||||
} else {
|
} else {
|
||||||
@ -253,6 +264,17 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn accepted_keyword(&mut self, keyword: Keyword) -> CompilationResult<'src, bool> {
|
||||||
|
let next = self.next()?;
|
||||||
|
|
||||||
|
if next.kind == Identifier && next.lexeme() == keyword.lexeme() {
|
||||||
|
self.advance()?;
|
||||||
|
Ok(true)
|
||||||
|
} else {
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Accept a dependency
|
/// Accept a dependency
|
||||||
fn accept_dependency(&mut self) -> CompilationResult<'src, Option<UnresolvedDependency<'src>>> {
|
fn accept_dependency(&mut self) -> CompilationResult<'src, Option<UnresolvedDependency<'src>>> {
|
||||||
if let Some(recipe) = self.accept_name()? {
|
if let Some(recipe) = self.accept_name()? {
|
||||||
@ -297,8 +319,8 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
|
|||||||
} else if self.accepted(Eof)? {
|
} else if self.accepted(Eof)? {
|
||||||
break;
|
break;
|
||||||
} else if self.next_is(Identifier) {
|
} else if self.next_is(Identifier) {
|
||||||
match next.lexeme() {
|
match Keyword::from_lexeme(next.lexeme()) {
|
||||||
keyword::ALIAS =>
|
Some(Keyword::Alias) =>
|
||||||
if self.next_are(&[Identifier, Identifier, Equals]) {
|
if self.next_are(&[Identifier, Identifier, Equals]) {
|
||||||
warnings.push(Warning::DeprecatedEquals {
|
warnings.push(Warning::DeprecatedEquals {
|
||||||
equals: self.get(2)?,
|
equals: self.get(2)?,
|
||||||
@ -309,20 +331,20 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
|
|||||||
} else {
|
} else {
|
||||||
items.push(Item::Recipe(self.parse_recipe(doc, false)?));
|
items.push(Item::Recipe(self.parse_recipe(doc, false)?));
|
||||||
},
|
},
|
||||||
keyword::EXPORT =>
|
Some(Keyword::Export) =>
|
||||||
if self.next_are(&[Identifier, Identifier, Equals]) {
|
if self.next_are(&[Identifier, Identifier, Equals]) {
|
||||||
warnings.push(Warning::DeprecatedEquals {
|
warnings.push(Warning::DeprecatedEquals {
|
||||||
equals: self.get(2)?,
|
equals: self.get(2)?,
|
||||||
});
|
});
|
||||||
self.presume_name(keyword::EXPORT)?;
|
self.presume_keyword(Keyword::Export)?;
|
||||||
items.push(Item::Assignment(self.parse_assignment(true)?));
|
items.push(Item::Assignment(self.parse_assignment(true)?));
|
||||||
} else if self.next_are(&[Identifier, Identifier, ColonEquals]) {
|
} else if self.next_are(&[Identifier, Identifier, ColonEquals]) {
|
||||||
self.presume_name(keyword::EXPORT)?;
|
self.presume_keyword(Keyword::Export)?;
|
||||||
items.push(Item::Assignment(self.parse_assignment(true)?));
|
items.push(Item::Assignment(self.parse_assignment(true)?));
|
||||||
} else {
|
} else {
|
||||||
items.push(Item::Recipe(self.parse_recipe(doc, false)?));
|
items.push(Item::Recipe(self.parse_recipe(doc, false)?));
|
||||||
},
|
},
|
||||||
keyword::SET =>
|
Some(Keyword::Set) =>
|
||||||
if self.next_are(&[Identifier, Identifier, ColonEquals]) {
|
if self.next_are(&[Identifier, Identifier, ColonEquals]) {
|
||||||
items.push(Item::Set(self.parse_set()?));
|
items.push(Item::Set(self.parse_set()?));
|
||||||
} else {
|
} else {
|
||||||
@ -363,7 +385,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
|
|||||||
|
|
||||||
/// Parse an alias, e.g `alias name := target`
|
/// Parse an alias, e.g `alias name := target`
|
||||||
fn parse_alias(&mut self) -> CompilationResult<'src, Alias<'src, Name<'src>>> {
|
fn parse_alias(&mut self) -> CompilationResult<'src, Alias<'src, Name<'src>>> {
|
||||||
self.presume_name(keyword::ALIAS)?;
|
self.presume_keyword(Keyword::Alias)?;
|
||||||
let name = self.parse_name()?;
|
let name = self.parse_name()?;
|
||||||
self.presume_any(&[Equals, ColonEquals])?;
|
self.presume_any(&[Equals, ColonEquals])?;
|
||||||
let target = self.parse_name()?;
|
let target = self.parse_name()?;
|
||||||
@ -386,6 +408,40 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
|
|||||||
|
|
||||||
/// Parse an expression, e.g. `1 + 2`
|
/// Parse an expression, e.g. `1 + 2`
|
||||||
fn parse_expression(&mut self) -> CompilationResult<'src, Expression<'src>> {
|
fn parse_expression(&mut self) -> CompilationResult<'src, Expression<'src>> {
|
||||||
|
if self.accepted_keyword(Keyword::If)? {
|
||||||
|
let lhs = self.parse_expression()?;
|
||||||
|
|
||||||
|
let inverted = self.accepted(BangEquals)?;
|
||||||
|
|
||||||
|
if !inverted {
|
||||||
|
self.expect(EqualsEquals)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let rhs = self.parse_expression()?;
|
||||||
|
|
||||||
|
self.expect(BraceL)?;
|
||||||
|
|
||||||
|
let then = self.parse_expression()?;
|
||||||
|
|
||||||
|
self.expect(BraceR)?;
|
||||||
|
|
||||||
|
self.expect_keyword(Keyword::Else)?;
|
||||||
|
|
||||||
|
self.expect(BraceL)?;
|
||||||
|
|
||||||
|
let otherwise = self.parse_expression()?;
|
||||||
|
|
||||||
|
self.expect(BraceR)?;
|
||||||
|
|
||||||
|
return Ok(Expression::Conditional {
|
||||||
|
lhs: Box::new(lhs),
|
||||||
|
rhs: Box::new(rhs),
|
||||||
|
then: Box::new(then),
|
||||||
|
otherwise: Box::new(otherwise),
|
||||||
|
inverted,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let value = self.parse_value()?;
|
let value = self.parse_value()?;
|
||||||
|
|
||||||
if self.accepted(Plus)? {
|
if self.accepted(Plus)? {
|
||||||
@ -619,37 +675,36 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
|
|||||||
|
|
||||||
/// Parse a setting
|
/// Parse a setting
|
||||||
fn parse_set(&mut self) -> CompilationResult<'src, Set<'src>> {
|
fn parse_set(&mut self) -> CompilationResult<'src, Set<'src>> {
|
||||||
self.presume_name(keyword::SET)?;
|
self.presume_keyword(Keyword::Set)?;
|
||||||
let name = Name::from_identifier(self.presume(Identifier)?);
|
let name = Name::from_identifier(self.presume(Identifier)?);
|
||||||
self.presume(ColonEquals)?;
|
self.presume(ColonEquals)?;
|
||||||
match name.lexeme() {
|
if name.lexeme() == Keyword::Shell.lexeme() {
|
||||||
keyword::SHELL => {
|
self.expect(BracketL)?;
|
||||||
self.expect(BracketL)?;
|
|
||||||
|
|
||||||
let command = self.parse_string_literal()?;
|
let command = self.parse_string_literal()?;
|
||||||
|
|
||||||
let mut arguments = Vec::new();
|
let mut arguments = Vec::new();
|
||||||
|
|
||||||
if self.accepted(Comma)? {
|
if self.accepted(Comma)? {
|
||||||
while !self.next_is(BracketR) {
|
while !self.next_is(BracketR) {
|
||||||
arguments.push(self.parse_string_literal()?);
|
arguments.push(self.parse_string_literal()?);
|
||||||
|
|
||||||
if !self.accepted(Comma)? {
|
if !self.accepted(Comma)? {
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
self.expect(BracketR)?;
|
self.expect(BracketR)?;
|
||||||
|
|
||||||
Ok(Set {
|
Ok(Set {
|
||||||
value: Setting::Shell(setting::Shell { command, arguments }),
|
value: Setting::Shell(setting::Shell { command, arguments }),
|
||||||
name,
|
name,
|
||||||
})
|
})
|
||||||
},
|
} else {
|
||||||
_ => Err(name.error(CompilationErrorKind::UnknownSetting {
|
Err(name.error(CompilationErrorKind::UnknownSetting {
|
||||||
setting: name.lexeme(),
|
setting: name.lexeme(),
|
||||||
})),
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1509,6 +1564,48 @@ mod tests {
|
|||||||
tree: (justfile (set shell "bash" "-cu" "-l")),
|
tree: (justfile (set shell "bash" "-cu" "-l")),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: conditional,
|
||||||
|
text: "a := if b == c { d } else { e }",
|
||||||
|
tree: (justfile (assignment a (if b == c d e))),
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: conditional_inverted,
|
||||||
|
text: "a := if b != c { d } else { e }",
|
||||||
|
tree: (justfile (assignment a (if b != c d e))),
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: conditional_concatinations,
|
||||||
|
text: "a := if b0 + b1 == c0 + c1 { d0 + d1 } else { e0 + e1 }",
|
||||||
|
tree: (justfile (assignment a (if (+ b0 b1) == (+ c0 c1) (+ d0 d1) (+ e0 e1)))),
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: conditional_nested_lhs,
|
||||||
|
text: "a := if if b == c { d } else { e } == c { d } else { e }",
|
||||||
|
tree: (justfile (assignment a (if (if b == c d e) == c d e))),
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: conditional_nested_rhs,
|
||||||
|
text: "a := if c == if b == c { d } else { e } { d } else { e }",
|
||||||
|
tree: (justfile (assignment a (if c == (if b == c d e) d e))),
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: conditional_nested_then,
|
||||||
|
text: "a := if b == c { if b == c { d } else { e } } else { e }",
|
||||||
|
tree: (justfile (assignment a (if b == c (if b == c d e) e))),
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: conditional_nested_otherwise,
|
||||||
|
text: "a := if b == c { d } else { if b == c { d } else { e } }",
|
||||||
|
tree: (justfile (assignment a (if b == c d (if b == c d e)))),
|
||||||
|
}
|
||||||
|
|
||||||
error! {
|
error! {
|
||||||
name: alias_syntax_multiple_rhs,
|
name: alias_syntax_multiple_rhs,
|
||||||
input: "alias foo = bar baz",
|
input: "alias foo = bar baz",
|
||||||
@ -1576,15 +1673,15 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
error! {
|
error! {
|
||||||
name: interpolation_outside_of_recipe,
|
name: unexpected_brace,
|
||||||
input: "{{",
|
input: "{{",
|
||||||
offset: 0,
|
offset: 0,
|
||||||
line: 0,
|
line: 0,
|
||||||
column: 0,
|
column: 0,
|
||||||
width: 2,
|
width: 1,
|
||||||
kind: UnexpectedToken {
|
kind: UnexpectedToken {
|
||||||
expected: vec![At, Comment, Eof, Eol, Identifier],
|
expected: vec![At, Comment, Eof, Eol, Identifier],
|
||||||
found: InterpolationStart,
|
found: BraceL,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -193,6 +193,13 @@ pub enum Expression {
|
|||||||
lhs: Box<Expression>,
|
lhs: Box<Expression>,
|
||||||
rhs: Box<Expression>,
|
rhs: Box<Expression>,
|
||||||
},
|
},
|
||||||
|
Conditional {
|
||||||
|
lhs: Box<Expression>,
|
||||||
|
rhs: Box<Expression>,
|
||||||
|
then: Box<Expression>,
|
||||||
|
otherwise: Box<Expression>,
|
||||||
|
inverted: bool,
|
||||||
|
},
|
||||||
String {
|
String {
|
||||||
text: String,
|
text: String,
|
||||||
},
|
},
|
||||||
@ -228,6 +235,19 @@ impl Expression {
|
|||||||
lhs: Box::new(Expression::new(lhs)),
|
lhs: Box::new(Expression::new(lhs)),
|
||||||
rhs: Box::new(Expression::new(rhs)),
|
rhs: Box::new(Expression::new(rhs)),
|
||||||
},
|
},
|
||||||
|
Conditional {
|
||||||
|
lhs,
|
||||||
|
rhs,
|
||||||
|
inverted,
|
||||||
|
then,
|
||||||
|
otherwise,
|
||||||
|
} => Expression::Conditional {
|
||||||
|
lhs: Box::new(Expression::new(lhs)),
|
||||||
|
rhs: Box::new(Expression::new(rhs)),
|
||||||
|
then: Box::new(Expression::new(lhs)),
|
||||||
|
otherwise: Box::new(Expression::new(rhs)),
|
||||||
|
inverted: *inverted,
|
||||||
|
},
|
||||||
StringLiteral { string_literal } => Expression::String {
|
StringLiteral { string_literal } => Expression::String {
|
||||||
text: string_literal.cooked.to_string(),
|
text: string_literal.cooked.to_string(),
|
||||||
},
|
},
|
||||||
|
@ -113,3 +113,16 @@ macro_rules! run_error {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
macro_rules! assert_matches {
|
||||||
|
($expression:expr, $( $pattern:pat )|+ $( if $guard:expr )?) => {
|
||||||
|
match $expression {
|
||||||
|
$( $pattern )|+ $( if $guard )? => {}
|
||||||
|
left => panic!(
|
||||||
|
"assertion failed: (left ~= right)\n left: `{:?}`\n right: `{}`",
|
||||||
|
left,
|
||||||
|
stringify!($($pattern)|+ $(if $guard)?)
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -5,6 +5,9 @@ pub(crate) enum TokenKind {
|
|||||||
Asterisk,
|
Asterisk,
|
||||||
At,
|
At,
|
||||||
Backtick,
|
Backtick,
|
||||||
|
BangEquals,
|
||||||
|
BraceL,
|
||||||
|
BraceR,
|
||||||
BracketL,
|
BracketL,
|
||||||
BracketR,
|
BracketR,
|
||||||
Colon,
|
Colon,
|
||||||
@ -15,6 +18,7 @@ pub(crate) enum TokenKind {
|
|||||||
Eof,
|
Eof,
|
||||||
Eol,
|
Eol,
|
||||||
Equals,
|
Equals,
|
||||||
|
EqualsEquals,
|
||||||
Identifier,
|
Identifier,
|
||||||
Indent,
|
Indent,
|
||||||
InterpolationEnd,
|
InterpolationEnd,
|
||||||
@ -36,6 +40,9 @@ impl Display for TokenKind {
|
|||||||
Asterisk => "'*'",
|
Asterisk => "'*'",
|
||||||
At => "'@'",
|
At => "'@'",
|
||||||
Backtick => "backtick",
|
Backtick => "backtick",
|
||||||
|
BangEquals => "'!='",
|
||||||
|
BraceL => "'{'",
|
||||||
|
BraceR => "'}'",
|
||||||
BracketL => "'['",
|
BracketL => "'['",
|
||||||
BracketR => "']'",
|
BracketR => "']'",
|
||||||
Colon => "':'",
|
Colon => "':'",
|
||||||
@ -46,6 +53,7 @@ impl Display for TokenKind {
|
|||||||
Eof => "end of file",
|
Eof => "end of file",
|
||||||
Eol => "end of line",
|
Eol => "end of line",
|
||||||
Equals => "'='",
|
Equals => "'='",
|
||||||
|
EqualsEquals => "'=='",
|
||||||
Identifier => "identifier",
|
Identifier => "identifier",
|
||||||
Indent => "indent",
|
Indent => "indent",
|
||||||
InterpolationEnd => "'}}'",
|
InterpolationEnd => "'}}'",
|
||||||
|
12
src/tree.rs
12
src/tree.rs
@ -41,6 +41,18 @@ macro_rules! tree {
|
|||||||
} => {
|
} => {
|
||||||
$crate::tree::Tree::atom("*")
|
$crate::tree::Tree::atom("*")
|
||||||
};
|
};
|
||||||
|
|
||||||
|
{
|
||||||
|
==
|
||||||
|
} => {
|
||||||
|
$crate::tree::Tree::atom("==")
|
||||||
|
};
|
||||||
|
|
||||||
|
{
|
||||||
|
!=
|
||||||
|
} => {
|
||||||
|
$crate::tree::Tree::atom("!=")
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A `Tree` is either…
|
/// A `Tree` is either…
|
||||||
|
@ -19,6 +19,19 @@ impl<'expression, 'src> Iterator for Variables<'expression, 'src> {
|
|||||||
| Some(Expression::StringLiteral { .. })
|
| Some(Expression::StringLiteral { .. })
|
||||||
| Some(Expression::Backtick { .. })
|
| Some(Expression::Backtick { .. })
|
||||||
| Some(Expression::Call { .. }) => None,
|
| Some(Expression::Call { .. }) => None,
|
||||||
|
Some(Expression::Conditional {
|
||||||
|
lhs,
|
||||||
|
rhs,
|
||||||
|
then,
|
||||||
|
otherwise,
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
|
self.stack.push(lhs);
|
||||||
|
self.stack.push(rhs);
|
||||||
|
self.stack.push(then);
|
||||||
|
self.stack.push(otherwise);
|
||||||
|
self.next()
|
||||||
|
},
|
||||||
Some(Expression::Variable { name, .. }) => Some(name.token()),
|
Some(Expression::Variable { name, .. }) => Some(name.token()),
|
||||||
Some(Expression::Concatination { lhs, rhs }) => {
|
Some(Expression::Concatination { lhs, rhs }) => {
|
||||||
self.stack.push(lhs);
|
self.stack.push(lhs);
|
||||||
|
158
tests/conditional.rs
Normal file
158
tests/conditional.rs
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
use crate::common::*;
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: then_branch_unevaluated,
|
||||||
|
justfile: "
|
||||||
|
foo:
|
||||||
|
echo {{ if 'a' == 'b' { `exit 1` } else { 'otherwise' } }}
|
||||||
|
",
|
||||||
|
stdout: "otherwise\n",
|
||||||
|
stderr: "echo otherwise\n",
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: otherwise_branch_unevaluated,
|
||||||
|
justfile: "
|
||||||
|
foo:
|
||||||
|
echo {{ if 'a' == 'a' { 'then' } else { `exit 1` } }}
|
||||||
|
",
|
||||||
|
stdout: "then\n",
|
||||||
|
stderr: "echo then\n",
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: otherwise_branch_unevaluated_inverted,
|
||||||
|
justfile: "
|
||||||
|
foo:
|
||||||
|
echo {{ if 'a' != 'b' { 'then' } else { `exit 1` } }}
|
||||||
|
",
|
||||||
|
stdout: "then\n",
|
||||||
|
stderr: "echo then\n",
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: then_branch_unevaluated_inverted,
|
||||||
|
justfile: "
|
||||||
|
foo:
|
||||||
|
echo {{ if 'a' != 'a' { `exit 1` } else { 'otherwise' } }}
|
||||||
|
",
|
||||||
|
stdout: "otherwise\n",
|
||||||
|
stderr: "echo otherwise\n",
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: complex_expressions,
|
||||||
|
justfile: "
|
||||||
|
foo:
|
||||||
|
echo {{ if 'a' + 'b' == `echo ab` { 'c' + 'd' } else { 'e' + 'f' } }}
|
||||||
|
",
|
||||||
|
stdout: "cd\n",
|
||||||
|
stderr: "echo cd\n",
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: undefined_lhs,
|
||||||
|
justfile: "
|
||||||
|
a := if b == '' { '' } else { '' }
|
||||||
|
|
||||||
|
foo:
|
||||||
|
echo {{ a }}
|
||||||
|
",
|
||||||
|
stdout: "",
|
||||||
|
stderr: "
|
||||||
|
error: Variable `b` not defined
|
||||||
|
|
|
||||||
|
1 | a := if b == '' { '' } else { '' }
|
||||||
|
| ^
|
||||||
|
",
|
||||||
|
status: EXIT_FAILURE,
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: undefined_rhs,
|
||||||
|
justfile: "
|
||||||
|
a := if '' == b { '' } else { '' }
|
||||||
|
|
||||||
|
foo:
|
||||||
|
echo {{ a }}
|
||||||
|
",
|
||||||
|
stdout: "",
|
||||||
|
stderr: "
|
||||||
|
error: Variable `b` not defined
|
||||||
|
|
|
||||||
|
1 | a := if '' == b { '' } else { '' }
|
||||||
|
| ^
|
||||||
|
",
|
||||||
|
status: EXIT_FAILURE,
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: undefined_then,
|
||||||
|
justfile: "
|
||||||
|
a := if '' == '' { b } else { '' }
|
||||||
|
|
||||||
|
foo:
|
||||||
|
echo {{ a }}
|
||||||
|
",
|
||||||
|
stdout: "",
|
||||||
|
stderr: "
|
||||||
|
error: Variable `b` not defined
|
||||||
|
|
|
||||||
|
1 | a := if '' == '' { b } else { '' }
|
||||||
|
| ^
|
||||||
|
",
|
||||||
|
status: EXIT_FAILURE,
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: undefined_otherwise,
|
||||||
|
justfile: "
|
||||||
|
a := if '' == '' { '' } else { b }
|
||||||
|
|
||||||
|
foo:
|
||||||
|
echo {{ a }}
|
||||||
|
",
|
||||||
|
stdout: "",
|
||||||
|
stderr: "
|
||||||
|
error: Variable `b` not defined
|
||||||
|
|
|
||||||
|
1 | a := if '' == '' { '' } else { b }
|
||||||
|
| ^
|
||||||
|
",
|
||||||
|
status: EXIT_FAILURE,
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: unexpected_op,
|
||||||
|
justfile: "
|
||||||
|
a := if '' a '' { '' } else { b }
|
||||||
|
|
||||||
|
foo:
|
||||||
|
echo {{ a }}
|
||||||
|
",
|
||||||
|
stdout: "",
|
||||||
|
stderr: "
|
||||||
|
error: Expected '!=', '==', or '+', but found identifier
|
||||||
|
|
|
||||||
|
1 | a := if '' a '' { '' } else { b }
|
||||||
|
| ^
|
||||||
|
",
|
||||||
|
status: EXIT_FAILURE,
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: dump,
|
||||||
|
justfile: "
|
||||||
|
a := if '' == '' { '' } else { '' }
|
||||||
|
|
||||||
|
foo:
|
||||||
|
echo {{ a }}
|
||||||
|
",
|
||||||
|
args: ("--dump"),
|
||||||
|
stdout: format!("
|
||||||
|
a := if '' == '' {{ '' }} else {{ '' }}{}
|
||||||
|
|
||||||
|
foo:
|
||||||
|
echo {{{{a}}}}
|
||||||
|
", " ").as_str(),
|
||||||
|
}
|
25
tests/error_messages.rs
Normal file
25
tests/error_messages.rs
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
use crate::common::*;
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: expected_keyword,
|
||||||
|
justfile: "foo := if '' == '' { '' } arlo { '' }",
|
||||||
|
stderr: "
|
||||||
|
error: Expected keyword `else` but found identifier `arlo`
|
||||||
|
|
|
||||||
|
1 | foo := if '' == '' { '' } arlo { '' }
|
||||||
|
| ^^^^
|
||||||
|
",
|
||||||
|
status: EXIT_FAILURE,
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: unexpected_character,
|
||||||
|
justfile: "!~",
|
||||||
|
stderr: "
|
||||||
|
error: Expected character `=`
|
||||||
|
|
|
||||||
|
1 | !~
|
||||||
|
| ^
|
||||||
|
",
|
||||||
|
status: EXIT_FAILURE,
|
||||||
|
}
|
@ -5,8 +5,10 @@ mod common;
|
|||||||
|
|
||||||
mod choose;
|
mod choose;
|
||||||
mod completions;
|
mod completions;
|
||||||
|
mod conditional;
|
||||||
mod dotenv;
|
mod dotenv;
|
||||||
mod edit;
|
mod edit;
|
||||||
|
mod error_messages;
|
||||||
mod examples;
|
mod examples;
|
||||||
mod init;
|
mod init;
|
||||||
mod interrupts;
|
mod interrupts;
|
||||||
|
Loading…
Reference in New Issue
Block a user