Browse Source

Match statements (#15)

* feat: lexical tokens for match

* feat: parser implementation of match

* feat: js generation for match statements

* feat: match block arms

* feat: default arm for match

* chore: fix formatting

* chore: fix clippy warnings

* docs: add match statement

* feat: use "else" keyword instead of "default"

Co-authored-by: Garrit Franke <garrit@slashdev.space>
master
Garrit Franke 9 months ago
committed by GitHub
parent
commit
278d5e6542
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 36
      docs/concepts/control-flow.md
  2. 1
      src/generator/c.rs
  3. 22
      src/generator/js.rs
  4. 8
      src/lexer/mod.rs
  5. 7
      src/parser/node_type.rs
  6. 8
      src/parser/parser.rs
  7. 53
      src/parser/rules.rs
  8. 37
      tests/match_statements.sb

36
docs/concepts/control-flow.md

@ -75,6 +75,42 @@ number is divisible by 3
When this program executes, it checks each `if` expression in turn and executes the first body for which the condition holds true. Note that even though 6 is divisible by 2, we don’t see the output `number is divisible by 2`, nor do we see the `number is not divisible by 4, 3, or 2` text from the else block. That’s because Sabre only executes the block for the first true condition, and once it finds one, it doesn’t even check the rest.
### Value matching
Working with `if` statements with multiple `else` branches can become tedious. `match` statements provide a cleaner syntax for this case. You can compare `match` statements to `switch` in many other languages. Let's look at a very simple match statement.
```
let x = 42
match x {
1 => println("x is 1")
2 => println("x is 2")
42 => println("The answer to the universe and everything!")
else => println("This will not be called")
}
```
In this example, we check the value of `x`, and execute some code based on the value. Instead of having to type `x == 1`, `x == 2` and so on, we instead provide the value only once, and decide what to do for each case. We can optionally provide a `else` case, which will be executed if no other case was triggered.
You can execute multiple statements inside a single case. A common case would be to log some debug output and then return a value.
```
fn invert(x: bool): bool {
match x {
true => {
println("The value is true")
return false
}
false => {
println("The value is false")
return true
}
}
}
```
Keep in mind that excessive use of this could hurt the readability of your code. Instead, you could try to outsource those statements into a function and call that instead.
## Loops
It's often useful to execute a block of code more than once. For this task, Sabre provides different kind of _loops_. A loop runs through the code inside the its body to the end and then starts immediately back at the beginning.

1
src/generator/c.rs

@ -112,6 +112,7 @@ fn generate_statement(statement: Statement) -> String {
Statement::For(_ident, _expr, _body) => todo!(),
Statement::Continue => todo!(),
Statement::Break => todo!(),
Statement::Match(_, _) => todo!(),
};
format!("{}\n", state)

22
src/generator/js.rs

@ -103,6 +103,7 @@ fn generate_statement(statement: Statement) -> String {
Statement::For(ident, expr, body) => generate_for_loop(ident, expr, *body),
Statement::Continue => generate_continue(),
Statement::Break => generate_break(),
Statement::Match(subject, arms) => generate_match(subject, arms),
};
format!("{};\n", state)
@ -165,6 +166,27 @@ fn generate_continue() -> String {
"continue;\n".into()
}
fn generate_match(subject: Expression, arms: Vec<MatchArm>) -> String {
let mut out_str = format!("switch ({E}) {{\n", E = generate_expression(subject));
for arm in arms {
match arm {
MatchArm::Case(expr, statement) => {
out_str += &format!("case {}:\n", generate_expression(expr));
out_str += &format!("{}\n", &generate_statement(statement));
out_str += "break;";
}
MatchArm::Else(statement) => {
out_str += "default:\n";
out_str += &format!("{}\n", &generate_statement(statement));
}
}
}
out_str += "}";
out_str
}
fn generate_array(elements: Vec<Expression>) -> String {
let mut out_str = String::from("[");

8
src/lexer/mod.rs

@ -104,6 +104,8 @@ pub enum TokenKind {
StarEqual,
/// "/="
SlashEqual,
/// "=>"
ArrowRight,
/// "("
BraceOpen,
/// ")"
@ -145,6 +147,7 @@ pub enum Keyword {
Boolean,
Struct,
New,
Match,
Unknown,
}
@ -244,6 +247,10 @@ impl Cursor<'_> {
self.bump();
Equals
}
'>' => {
self.bump();
ArrowRight
}
_ => Assign,
},
':' => Colon,
@ -368,6 +375,7 @@ impl Cursor<'_> {
c if c == "continue" => Keyword::Continue,
c if c == "struct" => Keyword::Struct,
c if c == "new" => Keyword::New,
c if c == "match" => Keyword::Match,
_ => Keyword::Unknown,
}
}

7
src/parser/node_type.rs

@ -97,6 +97,7 @@ pub enum Statement {
If(Expression, Box<Statement>, Option<Box<Statement>>),
While(Expression, Box<Statement>),
For(Variable, Expression, Box<Statement>),
Match(Expression, Vec<MatchArm>),
Break,
Continue,
Exp(Expression),
@ -141,6 +142,12 @@ impl TryFrom<Token> for Expression {
}
}
#[derive(Debug, Eq, PartialEq, Clone)]
pub enum MatchArm {
Case(Expression, Statement),
Else(Statement),
}
#[derive(Debug, Eq, PartialEq, Clone)]
pub enum BinOp {
Addition,

8
src/parser/parser.rs

@ -1,3 +1,4 @@
use crate::lexer::Keyword;
/**
* Copyright 2020 Garrit Franke
*
@ -13,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
use crate::lexer::Keyword;
use crate::lexer::Position;
use crate::lexer::{Token, TokenKind};
use crate::parser::infer::infer;
use crate::parser::node_type::*;
@ -126,6 +127,11 @@ impl Parser {
}
}
pub(super) fn make_error_msg(&mut self, pos: Position, msg: String) -> String {
let pos_string = format!("{}:{}", pos.line, pos.offset);
format!("ERROR: {}\nAt {}", msg, pos_string)
}
pub(super) fn prev(&mut self) -> Option<Token> {
self.prev.clone()
}

53
src/parser/rules.rs

@ -158,6 +158,7 @@ impl Parser {
fn parse_statement(&mut self) -> Result<Statement, String> {
let token = self.peek()?;
match &token.kind {
TokenKind::CurlyBracesOpen => self.parse_block(),
TokenKind::Keyword(Keyword::Let) => self.parse_declare(),
TokenKind::Keyword(Keyword::Return) => self.parse_return(),
TokenKind::Keyword(Keyword::If) => self.parse_conditional_statement(),
@ -165,6 +166,7 @@ impl Parser {
TokenKind::Keyword(Keyword::Break) => self.parse_break(),
TokenKind::Keyword(Keyword::Continue) => self.parse_continue(),
TokenKind::Keyword(Keyword::For) => self.parse_for_loop(),
TokenKind::Keyword(Keyword::Match) => self.parse_match_statement(),
TokenKind::Identifier(_) => {
let ident = self.match_identifier()?;
let expr = if self.peek_token(TokenKind::Dot).is_ok() {
@ -457,6 +459,57 @@ impl Parser {
))
}
fn parse_match_statement(&mut self) -> Result<Statement, String> {
self.match_keyword(Keyword::Match)?;
let subject = self.parse_expression()?;
self.match_token(TokenKind::CurlyBracesOpen)?;
let mut arms: Vec<MatchArm> = Vec::new();
// Used to mitigate multiple else cases were defined
let mut has_else = false;
loop {
let next = self.peek()?;
match next.kind {
TokenKind::Literal(_)
| TokenKind::Identifier(_)
| TokenKind::Keyword(Keyword::Boolean) => arms.push(self.parse_match_arm()?),
TokenKind::Keyword(Keyword::Else) => {
if has_else {
return Err(self.make_error_msg(
next.pos,
"Multiple else arms are not allowed".to_string(),
));
}
has_else = true;
arms.push(self.parse_match_arm()?);
}
TokenKind::CurlyBracesClose => break,
_ => return Err(self.make_error_msg(next.pos, "Illegal token".to_string())),
}
}
self.match_token(TokenKind::CurlyBracesClose)?;
Ok(Statement::Match(subject, arms))
}
fn parse_match_arm(&mut self) -> Result<MatchArm, String> {
let next = self.peek()?;
match next.kind {
TokenKind::Keyword(Keyword::Else) => {
self.match_keyword(Keyword::Else)?;
self.match_token(TokenKind::ArrowRight)?;
Ok(MatchArm::Else(self.parse_statement()?))
}
_ => {
let expr = self.parse_expression()?;
self.match_token(TokenKind::ArrowRight)?;
let statement = self.parse_statement()?;
Ok(MatchArm::Case(expr, statement))
}
}
}
fn parse_conditional_statement(&mut self) -> Result<Statement, String> {
self.match_keyword(Keyword::If)?;
let condition = self.parse_expression()?;

37
tests/match_statements.sb

@ -0,0 +1,37 @@
fn test_basic_match() {
let x = 1
match x {
1 => assert(true)
2 => assert(false)
}
}
fn test_boolean_match() {
let x = true
match x {
true => assert(true)
false => assert(false)
}
}
fn test_match_with_block_statement() {
let x = 42
match x {
1 => println("x is 1")
2 => {
println("This is a branch with multiple statements.")
println("x is 2, in case you are wondering")
}
42 => println("The answer to the universe and everything!")
else => println("Default case")
}
}
fn main() {
test_basic_match()
test_boolean_match()
test_match_with_block_statement()
}
Loading…
Cancel
Save