Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(syntax): add match operator #2267

Draft
wants to merge 10 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/assignment_resolver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,14 @@ impl<'src: 'run, 'run> AssignmentResolver<'src, 'run> {
Err(name.token.error(UndefinedVariable { variable }))
}
}
Expression::Match { expr, branches } => {
self.resolve_expression(expr)?;
for (check, then) in branches {
self.resolve_expression(check)?;
self.resolve_expression(then)?;
}
Ok(())
}
}
}
}
Expand Down
12 changes: 12 additions & 0 deletions src/evaluator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,18 @@ impl<'src, 'run> Evaluator<'src, 'run> {
})
}
}
Expression::Match { expr, branches } => {
let val = self.evaluate_expression(expr)?;
for (branch, next) in branches {
let check = self.evaluate_expression(branch)?;
if val == check || check == "_" {
return self.evaluate_expression(next);
}
}
Err(Error::Assert {
message: "invalid match statement, no branches matched".into(),
})
}
}
}

Expand Down
20 changes: 20 additions & 0 deletions src/expression.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ pub(crate) enum Expression<'src> {
then: Box<Expression<'src>>,
otherwise: Box<Expression<'src>>,
},
/// `match expr { branch_expr => then, ... }`
Match {
expr: Box<Expression<'src>>,
/// Pair of expression to compare to ('value'), along with the expression to actually execute
branches: Vec<(Expression<'src>, Expression<'src>)>,
},
/// `(contents)`
Group { contents: Box<Expression<'src>> },
/// `lhs / rhs`
Expand Down Expand Up @@ -70,6 +76,13 @@ impl<'src> Display for Expression<'src> {
Self::Variable { name } => write!(f, "{}", name.lexeme()),
Self::Call { thunk } => write!(f, "{thunk}"),
Self::Group { contents } => write!(f, "({contents})"),
Self::Match { expr, branches } => {
write!(f, "match {expr} {{ ")?;
for (branch, then) in branches {
write!(f, "{branch} => {then},")?;
}
write!(f, "\n}}")
}
}
}
}
Expand Down Expand Up @@ -128,6 +141,13 @@ impl<'src> Serialize for Expression<'src> {
seq.serialize_element(name)?;
seq.end()
}
Self::Match { expr, branches } => {
let mut seq = serializer.serialize_seq(None)?;
seq.serialize_element("match")?;
seq.serialize_element(expr)?;
seq.serialize_element(branches)?;
seq.end()
}
}
}
}
1 change: 1 addition & 0 deletions src/keyword.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ pub(crate) enum Keyword {
If,
IgnoreComments,
Import,
Match,
Mod,
PositionalArguments,
Quiet,
Expand Down
1 change: 1 addition & 0 deletions src/lexer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -960,6 +960,7 @@ mod tests {
Eol => "\n",
Equals => "=",
EqualsEquals => "==",
EqualsGreaterThan => "=>",
EqualsTilde => "=~",
Indent => " ",
InterpolationEnd => "}}",
Expand Down
9 changes: 9 additions & 0 deletions src/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,15 @@ impl<'src> Node<'src> for Expression<'src> {
lhs: Some(lhs),
rhs,
} => Tree::atom("/").push(lhs.tree()).push(rhs.tree()),
Self::Match { expr, branches } => {
let mut tree = Tree::atom(Keyword::Match.lexeme());
tree.push_mut(expr.tree());
for (check, then) in branches {
tree.push_mut(check.tree());
tree.push_mut(then.tree());
}
tree
}
}
}
}
Expand Down
35 changes: 35 additions & 0 deletions src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,8 @@ impl<'run, 'src> Parser<'run, 'src> {

let expression = if self.accepted_keyword(Keyword::If)? {
self.parse_conditional()?
} else if self.accepted_keyword(Keyword::Match)? {
self.parse_match()?
} else if self.accepted(Slash)? {
let lhs = None;
let rhs = self.parse_expression()?.into();
Expand Down Expand Up @@ -566,6 +568,33 @@ impl<'run, 'src> Parser<'run, 'src> {
})
}

/// Parse a match statement
///
/// e.g. `match a == b { true => "foo", _ => "bar" }`
fn parse_match(&mut self) -> CompileResult<'src, Expression<'src>> {
let expr = self.parse_expression()?;
let mut branches = Vec::new();
eprintln!("before EXPECT");
self.expect(BraceL)?;

// Parse as many values that lead to branches as we can
eprintln!("before parsing value");
while let Ok(value) = self.parse_value() {
eprintln!("value? [{value}]");
self.expect(EqualsGreaterThan)?;
let then = self.parse_expression()?;
let _ = self.expect(Comma);
branches.push((value, then));
}

self.expect(BraceR)?;

Ok(Expression::Match {
expr: expr.into(),
branches,
})
}

// Check if the next tokens are a shell-expanded string, i.e., `x"foo"`.
//
// This function skips initial whitespace tokens, but thereafter is
Expand Down Expand Up @@ -2123,6 +2152,12 @@ mod tests {
tree: (justfile (assignment a (if b == c d e))),
}

test! {
name: _match,
text: "a := match b == c { true => d, false => e }",
tree: (justfile (assignment a (match a == b true d false e))),
}

test! {
name: conditional_inverted,
text: "a := if b != c { d } else { e }",
Expand Down
11 changes: 11 additions & 0 deletions src/summary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,10 @@ pub enum Expression {
otherwise: Box<Expression>,
operator: ConditionalOperator,
},
Match {
expr: Box<Expression>,
branches: Vec<(Expression, Expression)>,
},
Join {
lhs: Option<Box<Expression>>,
rhs: Box<Expression>,
Expand Down Expand Up @@ -330,6 +334,13 @@ impl Expression {
name: name.lexeme().to_owned(),
},
Group { contents } => Self::new(contents),
Match { expr, branches } => Self::Match {
expr: Self::new(expr).into(),
branches: branches
.iter()
.map(|(check, branch)| (Self::new(check).into(), Self::new(branch).into()))
.collect(),
},
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions src/token_kind.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ pub(crate) enum TokenKind {
Eol,
Equals,
EqualsEquals,
EqualsGreaterThan,
EqualsTilde,
Identifier,
Indent,
Expand Down Expand Up @@ -65,6 +66,7 @@ impl Display for TokenKind {
Eol => "end of line",
Equals => "'='",
EqualsEquals => "'=='",
EqualsGreaterThan => "'=>'",
EqualsTilde => "'=~'",
Identifier => "identifier",
Indent => "indent",
Expand Down
7 changes: 7 additions & 0 deletions src/variables.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,13 @@ impl<'expression, 'src> Iterator for Variables<'expression, 'src> {
self.stack.push(rhs);
self.stack.push(lhs);
}
Expression::Match { expr, branches } => {
for (check, branch) in branches.iter().rev() {
self.stack.push(branch);
self.stack.push(check);
}
self.stack.push(expr);
}
}
}
}
Expand Down
154 changes: 154 additions & 0 deletions tests/match.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
use super::*;

test! {
name: other_branches_unevaluated,
justfile: "
foo:
echo {{ match 'a' == 'b' { true => `exit 1`, false => 'otherwise' } }}
",
stdout: "otherwise\n",
stderr: "echo otherwise\n",
}

test! {
name: otherwise_branch_unevaluated,
justfile: "
foo:
echo {{ match 'a' == 'a' { true => 'then', _ => `exit 1` } }}
",
stdout: "then\n",
stderr: "echo then\n",
}

test! {
name: complex_expressions,
justfile: "
foo:
echo {{ match 'a' + 'b' == `echo ab` { true => 'c' + 'd', false => 'e' + 'f' }}
",
stdout: "cd\n",
stderr: "echo cd\n",
}

test! {
name: undefined_expr,
justfile: "
a := match b == '' { _ => '' }

foo:
echo {{ a }}
",
stdout: "",
stderr: "
error: Variable `b` not defined
——▶ justfile:1:11
1 │ a := match b == '' { _ => '' }
│ ^
",
status: EXIT_FAILURE,
}

test! {
name: undefined_check,
justfile: "
a := match '' == '' { b => '' }

foo:
echo {{ a }}
",
stdout: "",
stderr: "
error: Variable `b` not defined
——▶ justfile:1:23
1 │ a := match '' == '' { b => '' }
│ ^
",
status: EXIT_FAILURE,
}

test! {
name: undefined_branch,
justfile: "
a := match '' == '' { true => b }

foo:
echo {{ a }}
",
stdout: "",
stderr: "
error: Variable `b` not defined
——▶ justfile:1:31
1 │ a := match '' == '' { true => b }
│ ^
",
status: EXIT_FAILURE,
}

test! {
name: undefined_otherwise,
justfile: "
a := match '' == '' { _ => b }

foo:
echo {{ a }}
",
stdout: "",
stderr: "
error: Variable `b` not defined
——▶ justfile:1:28
1 │ a := match '' == '' { _ => b }
│ ^
",
status: EXIT_FAILURE,
}

test! {
name: dump,
justfile: "
a := match '' == '' { _ => '' }

foo:
echo {{ a }}
",
args: ("--dump"),
stdout: "
a := match '' == '' { _ => '' }

foo:
echo {{ a }}
",
}

test! {
name: if_else,
justfile: "
x := match '0' == '1' { true => 'a', false => 'b' }

foo:
echo {{ x }}
",
stdout: "b\n",
stderr: "echo b\n",
}

// TODO: test for failed match

// test! {
// name: failed_match,
// justfile: "
// TEST := match '' == '' {}
// ",
// stdout: "",
// stderr: "
// error: Expected keyword `else` but found `end of line`
// ——▶ justfile:1:54
// │
// 1 │ TEST := if path_exists('/bin/bash') == 'true' {'yes'}
// │ ^
// ",
// status: EXIT_FAILURE,
// }
12 changes: 12 additions & 0 deletions tests/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,15 @@ fn dont_run_duplicate_recipes() {
)
.run();
}

#[test]
fn check_match() {
Test::new()
.justfile(
r#"
val := "yep"
computed := match val { _ => test }
"#,
)
.run();
}
Loading