diff --git a/README.MD b/README.MD index fe8fcd9..9bcb202 100644 --- a/README.MD +++ b/README.MD @@ -109,6 +109,26 @@ fn main() { let i = 100 for i in 0..3 { ... } // loop vars are scoped to the loop println(i) // 100 + + if score >= 90 { // else-if chains + grade = "A" + } else if score >= 80 { + grade = "B" + } else { + grade = "F" + } + + match day { // match on any comparable value + "sat" { + println("weekend") + } + "sun" { + println("weekend") + } + else { // optional fallback arm + println("workday") + } + } } ``` @@ -120,9 +140,12 @@ fn main() { `for i in 0...10 { }`; loop variables are scoped to the loop body - `break` / `continue` inside `while` and `for` loops (in `for` loops `continue` advances the loop variable / iterator first) +- else-if chains: `if a { } else if b { } else { }` +- `match`: `match expr { v1 { } v2 { } else { } }` — arms test equality on + any comparable value (ints, strings, ...); the `else` arm is optional - operators: `+ - * / %`, `== != < <= > >=`, `and or not`, unary `-` -- statements: `let`, assignment, `if/else`, `while`, `for`, `break`, - `continue`, `return`, `assert`, calls, `print(...)` / `println(...)` +- statements: `let`, assignment, `if/else`, `match`, `while`, `for`, + `break`, `continue`, `return`, `assert`, calls, `print(...)` / `println(...)` - comments: `//` ## Assembly diff --git a/compiler/compiler.v b/compiler/compiler.v index bfecf28..49ee4f0 100644 --- a/compiler/compiler.v +++ b/compiler/compiler.v @@ -190,6 +190,40 @@ fn (mut g Gen) gen_stmt(st Stmt) ! { } g.emit_label(end_l) } + .match_stmt { + // match x { v1 {..} v2 {..} else {..} } → subject := x; a chain of + // equality tests jumping to the matching arm; else falls through. + subj_idx := g.new_local() + end_l := g.new_label() + g.gen_expr(st.expr)! + g.emit_store(subj_idx) + for i, arm in st.arms { + next_l := g.new_label() + g.emit_load(subj_idx) + g.gen_expr(arm.val)! + g.code << op_eq + g.code << op_jz + g.code << obj.encode_i64(0) + g.fixups << Fixup{ name: next_l, off: u32(g.code.len) - 8 } + for s in arm.body { + g.gen_stmt(s)! + } + g.code << op_jmp + g.code << obj.encode_i64(0) + g.fixups << Fixup{ name: end_l, off: u32(g.code.len) - 8 } + g.emit_label(next_l) + if i == st.arms.len - 1 && !st.has_else { + // no else: fall through to the end label + g.emit_label(end_l) + } + } + if st.has_else { + for s in st.els_body { + g.gen_stmt(s)! + } + g.emit_label(end_l) + } + } .while_stmt { loop_l := g.new_label() end_l := g.new_label() diff --git a/compiler/lexer.v b/compiler/lexer.v index d0af92c..abf52ee 100644 --- a/compiler/lexer.v +++ b/compiler/lexer.v @@ -34,6 +34,7 @@ pub enum TokKind { kw_while kw_for kw_in + kw_match kw_break kw_continue kw_return @@ -253,6 +254,7 @@ fn (mut l Lexer) lex_ident(line int) Tok { 'while' { TokKind.kw_while } 'for' { TokKind.kw_for } 'in' { TokKind.kw_in } + 'match' { TokKind.kw_match } 'break' { TokKind.kw_break } 'continue' { TokKind.kw_continue } 'return' { TokKind.kw_return } diff --git a/compiler/parser.v b/compiler/parser.v index 1b808b7..e741b52 100644 --- a/compiler/parser.v +++ b/compiler/parser.v @@ -7,7 +7,8 @@ // stmt := 'let' IDENT '=' expr // | IDENT '=' expr // | postfix '=' expr (a[i] = v) -// | 'if' cond block ['else' block] +// | 'if' cond block ['else' ('if' ... | block)] +// | 'match' expr '{' (expr block | 'else' block)* '}' // | 'while' cond block // | 'for' IDENT 'in' range block (range := expr '..' expr | expr '...' expr) // | 'for' IDENT 'in' expr block (iterate an array) @@ -58,6 +59,7 @@ pub enum StmtKind { assign_stmt index_assign if_stmt + match_stmt while_stmt for_range_stmt for_in_stmt @@ -67,6 +69,13 @@ pub enum StmtKind { assert_stmt } +// MatchArm is a single `value { body }` arm of a match statement. +pub struct MatchArm { +pub mut: + val Expr + body []Stmt +} + pub struct Stmt { pub mut: kind StmtKind @@ -77,6 +86,9 @@ pub mut: idx Expr // index_assign: the index expression body []Stmt els []Stmt + arms []MatchArm // match_stmt: the arms (val + body) + has_else bool // match_stmt: a trailing else arm exists + els_body []Stmt // match_stmt: body of the else arm has_val bool inclusive bool // for_range_stmt: `..` (false) vs `...` (true) line int @@ -194,15 +206,31 @@ fn (mut p Parser) parse_stmt() !Stmt { e := p.parse_expr()! return Stmt{ kind: .let_stmt, target: name.lit, expr: e, line: t.line } } .kw_if { + return p.parse_if(t)! + } + .kw_match { p.advance() - cond := p.parse_cond()! - body := p.parse_block()! - mut els := []Stmt{} - if p.cur().kind == .kw_else { - p.advance() - els = p.parse_block()! + subject := p.parse_expr()! + p.expect(.lbrace, "'{'")! + mut arms := []MatchArm{} + mut has_else := false + mut els_body := []Stmt{} + for p.cur().kind != .rbrace { + if p.cur().kind == .eof { + return error('unexpected end of file inside match (missing "}")') + } + if p.cur().kind == .kw_else { + p.advance() + els_body = p.parse_block()! + has_else = true + continue + } + val := p.parse_expr()! + body := p.parse_block()! + arms << MatchArm{ val: val, body: body } } - return Stmt{ kind: .if_stmt, cond: cond, body: body, els: els, line: t.line } + p.expect(.rbrace, "'}'")! + return Stmt{ kind: .match_stmt, expr: subject, arms: arms, has_else: has_else, els_body: els_body, line: t.line } } .kw_while { p.advance() @@ -284,6 +312,25 @@ fn (mut p Parser) parse_stmt() !Stmt { } } +// parse_if parses `if cond block ['else' ('if' ... | block)]`. An `else if` +// chain is represented by putting the nested if-statement in the else list, +// so codegen needs no special casing. +fn (mut p Parser) parse_if(t Tok) !Stmt { + p.advance() + cond := p.parse_cond()! + body := p.parse_block()! + mut els := []Stmt{} + if p.cur().kind == .kw_else { + p.advance() + if p.cur().kind == .kw_if { + els << p.parse_if(p.cur())! + } else { + els = p.parse_block()! + } + } + return Stmt{ kind: .if_stmt, cond: cond, body: body, els: els, line: t.line } +} + // bin_node allocates a binary-operator node. It takes copies of the operands // so that `&l`/`&r` target fresh heap objects (taking the address of a local // that is later reassigned would create a self-referential node). diff --git a/examples/match.vr b/examples/match.vr new file mode 100644 index 0000000..e17216f --- /dev/null +++ b/examples/match.vr @@ -0,0 +1,81 @@ +// match.vr — else-if chains and match statements. +// vr run examples/match.vr + +// classify returns a label using an else-if chain. +fn classify(n) { + if n < 0 { + return "negative" + } else if n == 0 { + return "zero" + } else if n < 10 { + return "small" + } else { + return "big" + } +} + +// describe names a number with a match statement. +fn describe(n) { + match n { + 0 { + println("nothing") + } + 1 { + println("lonely") + } + 2 { + println("a pair") + } + 7 { + println("lucky") + } + else { + println("just " + n) + } + } +} + +fn main() { + // else-if chain + println(classify(-5)) + println(classify(0)) + println(classify(7)) + println(classify(100)) + + // match with else + describe(0) + describe(1) + describe(2) + describe(7) + describe(42) + + // match on strings + let day = "fri" + match day { + "sat" { + println("weekend!") + } + "sun" { + println("weekend!") + } + else { + println("workday") + } + } + + // match inside a loop (break/continue still work) + let found = 0 + for i in 0..10 { + match i { + 4 { + found = i + break + } + else { + // keep looking + } + } + } + println("found " + found) + assert found == 4 +} diff --git a/examples/tests.vr b/examples/tests.vr index eee77e7..1715089 100644 --- a/examples/tests.vr +++ b/examples/tests.vr @@ -125,6 +125,66 @@ fn test_break_continue() { assert odds[2] == 5 } +fn test_else_if() { + let score = 85 + let grade = "" + if score >= 90 { + grade = "A" + } else if score >= 80 { + grade = "B" + } else if score >= 70 { + grade = "C" + } else { + grade = "F" + } + assert grade == "B" + assert 10 > 5 +} + +fn test_match() { + let total = 0 + match 2 { + 1 { + total = total + 1 + } + 2 { + total = total + 10 + } + else { + total = total + 100 + } + } + assert total == 10 // matched the 2 arm + + // match on strings + let word = "cat" + let sound = "" + match word { + "dog" { + sound = "woof" + } + "cat" { + sound = "meow" + } + else { + sound = "?" + } + } + assert sound == "meow" + + // no else arm: nothing runs when nothing matches + let hit = 0 + match 99 { + 1 { + hit = 1 + } + 2 { + hit = 2 + } + } + assert hit == 0 +} + fn test_failing() { // this one is meant to fail — shows up in `vr test` output assert 1 == 2