diff --git a/docs/concepts/control-flow.md b/docs/concepts/control-flow.md index 369c5cc..d427b2b 100644 --- a/docs/concepts/control-flow.md +++ b/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. diff --git a/src/generator/c.rs b/src/generator/c.rs index ff799f2..70097e1 100644 --- a/src/generator/c.rs +++ b/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) diff --git a/src/generator/js.rs b/src/generator/js.rs index 6f174bf..5c37a88 100644 --- a/src/generator/js.rs +++ b/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) -> 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) -> String { let mut out_str = String::from("["); diff --git a/src/lexer/mod.rs b/src/lexer/mod.rs index 14586e4..73b3836 100644 --- a/src/lexer/mod.rs +++ b/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, } } diff --git a/src/parser/node_type.rs b/src/parser/node_type.rs index bc0940f..7a992e2 100644 --- a/src/parser/node_type.rs +++ b/src/parser/node_type.rs @@ -97,6 +97,7 @@ pub enum Statement { If(Expression, Box, Option>), While(Expression, Box), For(Variable, Expression, Box), + Match(Expression, Vec), Break, Continue, Exp(Expression), @@ -141,6 +142,12 @@ impl TryFrom for Expression { } } +#[derive(Debug, Eq, PartialEq, Clone)] +pub enum MatchArm { + Case(Expression, Statement), + Else(Statement), +} + #[derive(Debug, Eq, PartialEq, Clone)] pub enum BinOp { Addition, diff --git a/src/parser/parser.rs b/src/parser/parser.rs index 58e5751..486ece5 100644 --- a/src/parser/parser.rs +++ b/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 { self.prev.clone() } diff --git a/src/parser/rules.rs b/src/parser/rules.rs index 4334764..95aeafb 100644 --- a/src/parser/rules.rs +++ b/src/parser/rules.rs @@ -158,6 +158,7 @@ impl Parser { fn parse_statement(&mut self) -> Result { 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 { + self.match_keyword(Keyword::Match)?; + let subject = self.parse_expression()?; + self.match_token(TokenKind::CurlyBracesOpen)?; + let mut arms: Vec = 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 { + 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 { self.match_keyword(Keyword::If)?; let condition = self.parse_expression()?; diff --git a/tests/match_statements.sb b/tests/match_statements.sb new file mode 100644 index 0000000..e4a81f4 --- /dev/null +++ b/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() +}