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 3 years 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. 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 ## 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. 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::For(_ident, _expr, _body) => todo!(),
Statement::Continue => todo!(), Statement::Continue => todo!(),
Statement::Break => todo!(), Statement::Break => todo!(),
Statement::Match(_, _) => todo!(),
}; };
format!("{}\n", state) 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::For(ident, expr, body) => generate_for_loop(ident, expr, *body),
Statement::Continue => generate_continue(), Statement::Continue => generate_continue(),
Statement::Break => generate_break(), Statement::Break => generate_break(),
Statement::Match(subject, arms) => generate_match(subject, arms),
}; };
format!("{};\n", state) format!("{};\n", state)
@ -165,6 +166,27 @@ fn generate_continue() -> String {
"continue;\n".into() "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 { fn generate_array(elements: Vec<Expression>) -> String {
let mut out_str = String::from("["); let mut out_str = String::from("[");

8
src/lexer/mod.rs

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

7
src/parser/node_type.rs

@ -97,6 +97,7 @@ pub enum Statement {
If(Expression, Box<Statement>, Option<Box<Statement>>), If(Expression, Box<Statement>, Option<Box<Statement>>),
While(Expression, Box<Statement>), While(Expression, Box<Statement>),
For(Variable, Expression, Box<Statement>), For(Variable, Expression, Box<Statement>),
Match(Expression, Vec<MatchArm>),
Break, Break,
Continue, Continue,
Exp(Expression), 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)] #[derive(Debug, Eq, PartialEq, Clone)]
pub enum BinOp { pub enum BinOp {
Addition, Addition,

8
src/parser/parser.rs

@ -1,3 +1,4 @@
use crate::lexer::Keyword;
/** /**
* Copyright 2020 Garrit Franke * Copyright 2020 Garrit Franke
* *
@ -13,7 +14,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
use crate::lexer::Keyword; use crate::lexer::Position;
use crate::lexer::{Token, TokenKind}; use crate::lexer::{Token, TokenKind};
use crate::parser::infer::infer; use crate::parser::infer::infer;
use crate::parser::node_type::*; 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> { pub(super) fn prev(&mut self) -> Option<Token> {
self.prev.clone() self.prev.clone()
} }

53
src/parser/rules.rs

@ -158,6 +158,7 @@ impl Parser {
fn parse_statement(&mut self) -> Result<Statement, String> { fn parse_statement(&mut self) -> Result<Statement, String> {
let token = self.peek()?; let token = self.peek()?;
match &token.kind { match &token.kind {
TokenKind::CurlyBracesOpen => self.parse_block(),
TokenKind::Keyword(Keyword::Let) => self.parse_declare(), TokenKind::Keyword(Keyword::Let) => self.parse_declare(),
TokenKind::Keyword(Keyword::Return) => self.parse_return(), TokenKind::Keyword(Keyword::Return) => self.parse_return(),
TokenKind::Keyword(Keyword::If) => self.parse_conditional_statement(), TokenKind::Keyword(Keyword::If) => self.parse_conditional_statement(),
@ -165,6 +166,7 @@ impl Parser {
TokenKind::Keyword(Keyword::Break) => self.parse_break(), TokenKind::Keyword(Keyword::Break) => self.parse_break(),
TokenKind::Keyword(Keyword::Continue) => self.parse_continue(), TokenKind::Keyword(Keyword::Continue) => self.parse_continue(),
TokenKind::Keyword(Keyword::For) => self.parse_for_loop(), TokenKind::Keyword(Keyword::For) => self.parse_for_loop(),
TokenKind::Keyword(Keyword::Match) => self.parse_match_statement(),
TokenKind::Identifier(_) => { TokenKind::Identifier(_) => {
let ident = self.match_identifier()?; let ident = self.match_identifier()?;
let expr = if self.peek_token(TokenKind::Dot).is_ok() { 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> { fn parse_conditional_statement(&mut self) -> Result<Statement, String> {
self.match_keyword(Keyword::If)?; self.match_keyword(Keyword::If)?;
let condition = self.parse_expression()?; 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