1
0
mirror of https://github.com/vlang/v.git synced 2023-08-10 21:13:21 +03:00

all: add support for const ident in match range (#16526)

This commit is contained in:
Swastik Baranwal 2022-11-26 12:26:00 +05:30 committed by GitHub
parent dee75fe970
commit a9b41d2980
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 575 additions and 82 deletions

View File

@ -2,7 +2,8 @@
*Not yet released*
- `go foo()` has been replaced with `spawn foo()` (launches an OS thread, `go` will be used for
upcoming coroutines instead).
- vfmt now supports `// vfmt off` and `// vfmt on` for turning off the formatting locally for *short* snippets of code. Useful for keeping your carefully arranged matrices in tact.
- vfmt now supports `// vfmt off` and `// vfmt on` for turning off the formatting locally for *short* snippets of code. Useful for keeping your carefully arranged matrices intact.
- support match branch range expressions with consts: `match x { const1...const2 {} }`
## V 0.3.2
*31 Oct 2022*

View File

@ -1676,6 +1676,26 @@ Note that the ranges use `...` (three dots) rather than `..` (two dots). This is
because the range is *inclusive* of the last element, rather than exclusive
(as `..` ranges are). Using `..` in a match branch will throw an error.
```v
const start = 1
const end = 10
c := 2
num := match c {
start...end {
1000
}
else {
0
}
}
println(num)
// 1000
```
Constants can also be used in the range branch expressions.
Note: `match` as an expression is not usable in `for` loop and `if` statements.
### In operator

View File

@ -1325,6 +1325,7 @@ pub:
pub mut:
low Expr
high Expr
typ Type // filled in by checker; the type of `0...1` is `int` for example, while `a`...`z` is `rune` etc
}
[minify]

View File

@ -12,8 +12,19 @@ fn (mut c Checker) check_types(got ast.Type, expected ast.Type) bool {
}
got_is_ptr := got.is_ptr()
exp_is_ptr := expected.is_ptr()
got_is_int := got.is_int()
exp_is_int := expected.is_int()
exp_is_pure_int := expected.is_pure_int()
got_is_pure_int := got.is_pure_int()
// allow int literals where any kind of real integers are expected:
if (exp_is_pure_int && got == ast.int_literal_type)
|| (got_is_pure_int && expected == ast.int_literal_type) {
return true
}
if c.pref.translated {
if expected.is_int() && got.is_int() {
if exp_is_int && got_is_int {
return true
}
if expected == ast.byteptr_type {
@ -22,8 +33,8 @@ fn (mut c Checker) check_types(got ast.Type, expected ast.Type) bool {
if expected == ast.voidptr_type || expected == ast.nil_type {
return true
}
if (expected == ast.bool_type && (got.is_any_kind_of_pointer() || got.is_int()))
|| ((expected.is_any_kind_of_pointer() || expected.is_int()) && got == ast.bool_type) {
if (expected == ast.bool_type && (got.is_any_kind_of_pointer() || got_is_int))
|| ((expected.is_any_kind_of_pointer() || exp_is_int) && got == ast.bool_type) {
return true
}
@ -38,8 +49,7 @@ fn (mut c Checker) check_types(got ast.Type, expected ast.Type) bool {
}
// allow rune -> any int and vice versa
if (expected == ast.rune_type && got.is_int())
|| (got == ast.rune_type && expected.is_int()) {
if (expected == ast.rune_type && got_is_int) || (got == ast.rune_type && exp_is_int) {
return true
}
got_sym := c.table.sym(got)
@ -546,6 +556,9 @@ fn (mut c Checker) promote_keeping_aliases(left_type ast.Type, right_type ast.Ty
}
fn (mut c Checker) promote(left_type ast.Type, right_type ast.Type) ast.Type {
if left_type == right_type {
return left_type // strings, self defined operators
}
if left_type.is_any_kind_of_pointer() {
if right_type.is_int() || c.pref.translated {
return left_type
@ -559,9 +572,6 @@ fn (mut c Checker) promote(left_type ast.Type, right_type ast.Type) ast.Type {
return ast.void_type
}
}
if left_type == right_type {
return left_type // strings, self defined operators
}
if right_type.is_number() && left_type.is_number() {
return c.promote_num(left_type, right_type)
} else if left_type.has_flag(.optional) != right_type.has_flag(.optional) {

View File

@ -2452,8 +2452,20 @@ pub fn (mut c Checker) expr(node_ ast.Expr) ast.Type {
return c.expr(node.expr)
}
ast.RangeExpr {
// never happens
return ast.void_type
// branch range expression of `match x { a...b {} }`, or: `a#[x..y]`:
ltyp := c.expr(node.low)
htyp := c.expr(node.high)
if !c.check_types(ltyp, htyp) {
lstype := c.table.type_to_str(ltyp)
hstype := c.table.type_to_str(htyp)
c.add_error_detail('')
c.add_error_detail(' low part type: ${lstype}')
c.add_error_detail('high part type: ${hstype}')
c.error('the low and high parts of a range expression, should have matching types',
node.pos)
}
node.typ = c.promote(ltyp, htyp)
return ltyp
}
ast.SelectExpr {
return c.select_expr(mut node)

View File

@ -139,66 +139,98 @@ fn (mut c Checker) check_match_branch_last_stmt(last_stmt ast.ExprStmt, ret_type
}
}
fn (mut c Checker) get_comptime_number_value(mut expr ast.Expr) ?i64 {
if mut expr is ast.CharLiteral {
return expr.val[0]
}
if mut expr is ast.IntegerLiteral {
return expr.val.i64()
}
if mut expr is ast.Ident {
if mut obj := c.table.global_scope.find_const(expr.name) {
if obj.typ == 0 {
obj.typ = c.expr(obj.expr)
}
return c.get_comptime_number_value(mut obj.expr)
}
}
return none
}
fn (mut c Checker) match_exprs(mut node ast.MatchExpr, cond_type_sym ast.TypeSymbol) {
c.expected_type = node.expected_type
cond_sym := c.table.sym(node.cond_type)
// branch_exprs is a histogram of how many times
// an expr was used in the match
mut branch_exprs := map[string]int{}
for branch_i, _ in node.branches {
mut branch := node.branches[branch_i]
mut expr_types := []ast.TypeNode{}
for k, expr in branch.exprs {
for k, mut expr in branch.exprs {
mut key := ''
if expr is ast.RangeExpr {
// TODO: investigate why enums are different here:
if expr !is ast.EnumVal {
// ensure that the sub expressions of the branch are actually checked, before anything else:
_ := c.expr(expr)
}
if mut expr is ast.RangeExpr {
// Allow for `match enum_value { 4..5 { } }`, even though usually int and enum values,
// are considered incompatible outside unsafe{}, and are not allowed to be compared directly
if cond_sym.kind != .enum_ && !c.check_types(expr.typ, node.cond_type) {
mcstype := c.table.type_to_str(node.cond_type)
brstype := c.table.type_to_str(expr.typ)
c.add_error_detail('')
c.add_error_detail('match condition type: ${mcstype}')
c.add_error_detail(' range type: ${brstype}')
c.error('the range type and the match condition type should match',
expr.pos)
}
mut low_value_higher_than_high_value := false
mut low := i64(0)
mut high := i64(0)
c.expected_type = node.expected_type
low_expr := expr.low
high_expr := expr.high
final_cond_sym := c.table.final_sym(node.cond_type)
if low_expr is ast.IntegerLiteral {
if high_expr is ast.IntegerLiteral
&& (final_cond_sym.is_int() || final_cond_sym.info is ast.Enum) {
low = low_expr.val.i64()
high = high_expr.val.i64()
if low > high {
c.error('start value is higher than end value', branch.pos)
mut both_low_and_high_are_known := false
if low_value := c.get_comptime_number_value(mut expr.low) {
low = low_value
if high_value := c.get_comptime_number_value(mut expr.high) {
high = high_value
both_low_and_high_are_known = true
if low_value > high_value {
low_value_higher_than_high_value = true
}
} else {
c.error('mismatched range types - ${expr.low} is an integer, but ${expr.high} is not',
low_expr.pos)
}
} else if low_expr is ast.CharLiteral {
if high_expr is ast.CharLiteral && final_cond_sym.kind in [.u8, .char, .rune] {
low = low_expr.val[0]
high = high_expr.val[0]
if low > high {
c.error('start value is higher than end value', branch.pos)
if expr.high !is ast.EnumVal {
c.error('match branch range expressions need the end value to be known at compile time (only enums, const or literals are supported)',
expr.high.pos())
}
} else {
typ := c.table.type_to_str(c.expr(node.cond))
c.error('mismatched range types - trying to match `${node.cond}`, which has type `${typ}`, against a range of `rune`',
low_expr.pos)
}
} else {
typ := c.table.type_to_str(c.expr(expr.low))
c.error('cannot use type `${typ}` in match range', branch.pos)
if expr.low !is ast.EnumVal {
c.error('match branch range expressions need the start value to be known at compile time (only enums, const or literals are supported)',
expr.low.pos())
}
}
high_low_cutoff := 1000
if high - low > high_low_cutoff {
c.warn('more than ${high_low_cutoff} possibilities (${low} ... ${high}) in match range',
if low_value_higher_than_high_value {
c.error('the start value `${low}` should be lower than the end value `${high}`',
branch.pos)
}
for i in low .. high + 1 {
key = i.str()
val := if key in branch_exprs { branch_exprs[key] } else { 0 }
if val == 1 {
c.error('match case `${key}` is handled more than once', branch.pos)
if both_low_and_high_are_known {
high_low_cutoff := 1000
if high - low > high_low_cutoff {
c.warn('more than ${high_low_cutoff} possibilities (${low} ... ${high}) in match range',
branch.pos)
}
for i in low .. high + 1 {
key = i.str()
val := if key in branch_exprs { branch_exprs[key] } else { 0 }
if val == 1 {
c.error('match case `${key}` is handled more than once', branch.pos)
}
branch_exprs[key] = val + 1
}
branch_exprs[key] = val + 1
}
continue
}
match expr {
match mut expr {
ast.TypeNode {
key = c.table.type_to_str(expr.typ)
expr_types << expr
@ -207,7 +239,7 @@ fn (mut c Checker) match_exprs(mut node ast.MatchExpr, cond_type_sym ast.TypeSym
key = expr.val
}
else {
key = expr.str()
key = (*expr).str()
}
}
val := if key in branch_exprs { branch_exprs[key] } else { 0 }

View File

@ -0,0 +1,20 @@
vlib/v/checker/tests/const_expr_match_range_invalid_err.vv:6:2: error: the range type and the match condition type should match
4 |
5 | match 5 {
6 | start...`1` {
| ~~~~~~~~~~~
7 | println(start)
8 | }
Details:
match condition type: int
range type: rune
vlib/v/checker/tests/const_expr_match_range_invalid_err.vv:9:2: error: the low and high parts of a range expression, should have matching types
7 | println(start)
8 | }
9 | 'str'...end {
| ~~~~~~~~~~~
10 | println(end)
11 | }
Details:
low part type: string
high part type: int literal

View File

@ -0,0 +1,13 @@
const start = 1
const end = 20
match 5 {
start...`1` {
println(start)
}
'str'...end {
println(end)
}
else {}
}

View File

@ -0,0 +1,20 @@
vlib/v/checker/tests/const_match_invalid_type_range_err.vv:6:2: error: the range type and the match condition type should match
4 |
5 | match 5 {
6 | start...end {
| ~~~~~~~~~~~
7 | println(start)
8 | }
Details:
match condition type: int
range type: rune
vlib/v/checker/tests/const_match_invalid_type_range_err.vv:13:2: error: the range type and the match condition type should match
11 |
12 | b := match 5 {
13 | start...end {
| ~~~~~~~~~~~
14 | 3
15 | }
Details:
match condition type: int
range type: rune

View File

@ -0,0 +1,20 @@
const start = `a`
const end = 2
match 5 {
start...end {
println(start)
}
else {}
}
b := match 5 {
start...end {
3
}
else {
2
}
}
println(b)

View File

@ -0,0 +1,40 @@
vlib/v/checker/tests/const_match_mismatch_end_range_err.vv:6:2: error: the range type and the match condition type should match
4 |
5 | match 5 {
6 | 0...end {
| ~~~~~~~
7 | println(start)
8 | }
Details:
match condition type: int
range type: rune
vlib/v/checker/tests/const_match_mismatch_end_range_err.vv:9:2: error: the range type and the match condition type should match
7 | println(start)
8 | }
9 | `a`...start {
| ~~~~~~~~~~~
10 | println(end)
11 | }
Details:
match condition type: int
range type: rune
vlib/v/checker/tests/const_match_mismatch_end_range_err.vv:16:2: error: the range type and the match condition type should match
14 |
15 | a := match 5 {
16 | 0...end {
| ~~~~~~~
17 | 1
18 | }
Details:
match condition type: int
range type: rune
vlib/v/checker/tests/const_match_mismatch_end_range_err.vv:19:2: error: the range type and the match condition type should match
17 | 1
18 | }
19 | `a`...start {
| ~~~~~~~~~~~
20 | `a`
21 | }
Details:
match condition type: int
range type: rune

View File

@ -0,0 +1,26 @@
const start = 12
const end = `a`
match 5 {
0...end {
println(start)
}
`a`...start {
println(end)
}
else {}
}
a := match 5 {
0...end {
1
}
`a`...start {
`a`
}
else {
2
}
}
println(a)

View File

@ -0,0 +1,14 @@
vlib/v/checker/tests/const_match_range_duplicate_case_err.vv:9:2: error: match case `2` is handled more than once
7 | println(1)
8 | }
9 | start...end {
| ~~~~~~~~~~~
10 | println(3)
11 | }
vlib/v/checker/tests/const_match_range_duplicate_case_err.vv:21:2: error: match case `2` is handled more than once
19 | 1
20 | }
21 | start...end {
| ~~~~~~~~~~~
22 | 3
23 | }

View File

@ -0,0 +1,28 @@
const start = 2
const end = 12
match 5 {
start...end {
println(1)
}
start...end {
println(3)
}
else {
println(2)
}
}
a := match 5 {
start...end {
1
}
start...end {
3
}
else {
2
}
}
println(a)

View File

@ -0,0 +1,14 @@
vlib/v/checker/tests/const_match_range_invalid_err.vv:6:2: error: the start value `10` should be lower than the end value `1`
4 |
5 | match 5 {
6 | end...start {
| ~~~~~~~~~~~
7 | println(start)
8 | }
vlib/v/checker/tests/const_match_range_invalid_err.vv:13:2: error: the start value `10` should be lower than the end value `1`
11 |
12 | a := match 5 {
13 | end...start {
| ~~~~~~~~~~~
14 | 4
15 | }

View File

@ -0,0 +1,20 @@
const end = 10
const start = 1
match 5 {
end...start {
println(start)
}
else {}
}
a := match 5 {
end...start {
4
}
else {
3
}
}
println(a)

View File

@ -0,0 +1,20 @@
vlib/v/checker/tests/const_match_type_mismatch_range_err.vv:6:2: error: the low and high parts of a range expression, should have matching types
4 |
5 | match 5 {
6 | start...end {
| ~~~~~~~~~~~
7 | println(start)
8 | }
Details:
low part type: string
high part type: int literal
vlib/v/checker/tests/const_match_type_mismatch_range_err.vv:13:2: error: the low and high parts of a range expression, should have matching types
11 |
12 | z := match 5 {
13 | start...end {
| ~~~~~~~~~~~
14 | 2
15 | }
Details:
low part type: string
high part type: int literal

View File

@ -0,0 +1,20 @@
const end = 10
const start = 'str'
match 5 {
start...end {
println(start)
}
else {}
}
z := match 5 {
start...end {
2
}
else {
5
}
}
println(z)

View File

@ -0,0 +1,28 @@
vlib/v/checker/tests/invalid_const_expr_match_range_err.vv:5:2: error: match branch range expressions need the start value to be known at compile time (only enums, const or literals are supported)
3 |
4 | match 5 {
5 | start...10 {
| ~~~~~
6 | println(start)
7 | }
vlib/v/checker/tests/invalid_const_expr_match_range_err.vv:8:7: error: match branch range expressions need the end value to be known at compile time (only enums, const or literals are supported)
6 | println(start)
7 | }
8 | 10...end {
| ~~~
9 | println(end)
10 | }
vlib/v/checker/tests/invalid_const_expr_match_range_err.vv:15:2: error: match branch range expressions need the start value to be known at compile time (only enums, const or literals are supported)
13 |
14 | a := match 5 {
15 | start...10 {
| ~~~~~
16 | 2
17 | }
vlib/v/checker/tests/invalid_const_expr_match_range_err.vv:18:7: error: match branch range expressions need the end value to be known at compile time (only enums, const or literals are supported)
16 | 2
17 | }
18 | 10...end {
| ~~~
19 | 3
20 | }

View File

@ -0,0 +1,25 @@
start := 1
end := 20
match 5 {
start...10 {
println(start)
}
10...end {
println(end)
}
else {}
}
a := match 5 {
start...10 {
2
}
10...end {
3
}
else {
1
}
}
println(a)

View File

@ -1,4 +1,4 @@
vlib/v/checker/tests/match_expr_range_low_higher_than_high.vv:5:3: error: start value is higher than end value
vlib/v/checker/tests/match_expr_range_low_higher_than_high.vv:5:3: error: the start value `20` should be lower than the end value `10`
3 | value := 10
4 | match value {
5 | 20...10 {}

View File

@ -1,14 +1,20 @@
vlib/v/checker/tests/match_range_mismatch_type_err.vv:4:3: error: mismatched range types - trying to match `x`, which has type `string`, against a range of `rune`
vlib/v/checker/tests/match_range_mismatch_type_err.vv:4:3: error: the range type and the match condition type should match
2 | x := '1'
3 | match x {
4 | `0`...`9` {
| ~~~
| ~~~~~~~~~
5 | println('0-9')
6 | }
vlib/v/checker/tests/match_range_mismatch_type_err.vv:16:3: error: mismatched range types - 0 is an integer, but `9` is not
Details:
match condition type: string
range type: rune
vlib/v/checker/tests/match_range_mismatch_type_err.vv:16:3: error: the range type and the match condition type should match
14 | x := 1
15 | match x {
16 | 0...`9` {}
| ^
| ~~~~~~~
17 | else {}
18 | }
Details:
match condition type: int
range type: rune

View File

@ -0,0 +1,14 @@
vlib/v/checker/tests/non_const_match_range_err.vv:6:2: error: match branch range expressions need the start value to be known at compile time (only enums, const or literals are supported)
4 |
5 | match 5 {
6 | start...end {
| ~~~~~
7 | println(start)
8 | }
vlib/v/checker/tests/non_const_match_range_err.vv:13:2: error: match branch range expressions need the start value to be known at compile time (only enums, const or literals are supported)
11 |
12 | a := match 5 {
13 | start...end {
| ~~~~~
14 | 0
15 | }

View File

@ -0,0 +1,20 @@
const end = 10
start := 1
match 5 {
start...end {
println(start)
}
else {}
}
a := match 5 {
start...end {
0
}
else {
1
}
}
println(a)

View File

@ -8,7 +8,7 @@ import v.util
fn (mut g Gen) index_expr(node ast.IndexExpr) {
if node.index is ast.RangeExpr {
g.range_expr(node, node.index)
g.index_range_expr(node, node.index)
} else {
sym := g.table.final_sym(g.unwrap_generic(node.left_type))
if sym.kind == .array {
@ -57,7 +57,7 @@ fn (mut g Gen) index_expr(node ast.IndexExpr) {
}
}
fn (mut g Gen) range_expr(node ast.IndexExpr, range ast.RangeExpr) {
fn (mut g Gen) index_range_expr(node ast.IndexExpr, range ast.RangeExpr) {
sym := g.table.final_sym(node.left_type)
mut tmp_opt := ''
mut cur_line := ''

View File

@ -252,15 +252,8 @@ fn (mut g Gen) match_expr_switch(node ast.MatchExpr, is_expr bool, cond_var stri
g.write(' || ')
}
if expr is ast.RangeExpr {
// if type is unsigned and low is 0, check is unneeded
mut skip_low := false
if expr.low is ast.IntegerLiteral {
if node_cond_type_unsigned && expr.low.val == '0' {
skip_low = true
}
}
g.write('(')
if !skip_low {
if g.should_check_low_bound_in_range_expr(expr, node_cond_type_unsigned) {
g.write('${cond_var} >= ')
g.expr(expr.low)
g.write(' && ')
@ -324,15 +317,8 @@ fn (mut g Gen) match_expr_switch(node ast.MatchExpr, is_expr bool, cond_var stri
g.write(' || ')
}
if expr is ast.RangeExpr {
// if type is unsigned and low is 0, check is unneeded
mut skip_low := false
if expr.low is ast.IntegerLiteral {
if node_cond_type_unsigned && expr.low.val == '0' {
skip_low = true
}
}
g.write('(')
if !skip_low {
if g.should_check_low_bound_in_range_expr(expr, node_cond_type_unsigned) {
g.write('${cond_var} >= ')
g.expr(expr.low)
g.write(' && ')
@ -359,6 +345,28 @@ fn (mut g Gen) match_expr_switch(node ast.MatchExpr, is_expr bool, cond_var stri
g.writeln('}')
}
fn (mut g Gen) should_check_low_bound_in_range_expr(expr ast.RangeExpr, node_cond_type_unsigned bool) bool {
// if the type is unsigned, and the low bound of the range expression is 0,
// checking it at runtime is not needed:
mut should_check_low_bound := true
if node_cond_type_unsigned {
if expr.low is ast.IntegerLiteral {
if expr.low.val == '0' {
should_check_low_bound = false
}
} else if expr.low is ast.Ident {
if mut obj := g.table.global_scope.find_const(expr.low.name) {
if mut obj.expr is ast.IntegerLiteral {
if obj.expr.val == '0' {
should_check_low_bound = false
}
}
}
}
}
return should_check_low_bound
}
fn (mut g Gen) match_expr_classic(node ast.MatchExpr, is_expr bool, cond_var string, tmp_var string) {
node_cond_type_unsigned := node.cond_type in [ast.u16_type, ast.u32_type, ast.u64_type]
type_sym := g.table.sym(node.cond_type)
@ -431,15 +439,8 @@ fn (mut g Gen) match_expr_classic(node ast.MatchExpr, is_expr bool, cond_var str
}
else {
if expr is ast.RangeExpr {
// if type is unsigned and low is 0, check is unneeded
mut skip_low := false
if expr.low is ast.IntegerLiteral {
if node_cond_type_unsigned && expr.low.val == '0' {
skip_low = true
}
}
g.write('(')
if !skip_low {
if g.should_check_low_bound_in_range_expr(expr, node_cond_type_unsigned) {
g.write('${cond_var} >= ')
g.expr(expr.low)
g.write(' && ')

View File

@ -267,6 +267,7 @@ fn (mut p Parser) match_expr() ast.MatchExpr {
// Expression match
for {
p.inside_match_case = true
mut range_pos := p.tok.pos()
expr := p.expr(0)
ecmnts << p.eat_comments()
p.inside_match_case = false
@ -276,13 +277,15 @@ fn (mut p Parser) match_expr() ast.MatchExpr {
return ast.MatchExpr{}
} else if p.tok.kind == .ellipsis {
p.next()
p.inside_match_case = true
expr2 := p.expr(0)
p.inside_match_case = false
exprs << ast.RangeExpr{
low: expr
high: expr2
has_low: true
has_high: true
pos: p.tok.pos()
pos: range_pos.extend(p.prev_tok.pos())
}
} else {
exprs << expr

View File

@ -0,0 +1,65 @@
const (
start = 1
start_2 = 4
end = 3
end_2 = 8
//
start_rune = `a`
start_2_rune = `d`
end_rune = `c`
end_2_rune = `i`
)
fn test_match_int_const_ranges() {
mut results := []int{}
for x in 0 .. 10 {
match x {
start...end { results << 1 }
start_2...5 { results << 2 }
6...end_2 { results << 3 }
else { results << 4 }
}
}
assert results == [4, 1, 1, 1, 2, 2, 3, 3, 3, 4]
}
fn test_match_rune_const_ranges() {
mut results := []int{}
for x in `a` .. `l` {
match x {
start_rune...end_rune { results << 1 }
start_2_rune...`e` { results << 2 }
`f`...end_2_rune { results << 3 }
else { results << 4 }
}
}
assert results == [1, 1, 1, 2, 2, 3, 3, 3, 3, 4, 4]
}
fn test_match_expr_int_const_ranges() {
mut results := []int{}
for x in 0 .. 10 {
result := match x {
start...end { 1 }
start_2...5 { 2 }
6...end_2 { 3 }
else { 4 }
}
results << result
}
assert results == [4, 1, 1, 1, 2, 2, 3, 3, 3, 4]
}
fn test_match_expr_rune_const_ranges() {
mut results := []int{}
for x in `a` .. `l` {
result := match x {
start_rune...end_rune { 1 }
start_2_rune...`e` { 2 }
`f`...end_2_rune { 3 }
else { 4 }
}
results << result
}
assert results == [1, 1, 1, 2, 2, 3, 3, 3, 3, 4, 4]
}