Statements & expressions

If

a := 10 b := 20 if a < b { println('${a} < ${b}') } else if a > b { println('${a} > ${b}') } else { println('${a} == ${b}') }

if statements are pretty straightforward and similar to most other languages. Unlike other C-like languages, there are no parentheses surrounding the condition and the braces are always required.

If expressions

Unlike C, V does not have a ternary operator, that would allow you to do: x = c ? 1 : 2 . Instead, it has a bit more verbose, but also clearer to read, ability to use if as an expression. The direct translation in V of the ternary construct above, assuming c is a boolean condition, would be: x = if c { 1 } else { 2 }.

Here is another example:

num := 777 s := if num % 2 == 0 { 'even' } else { 'odd' } println(s) // "odd"

You can use multiple statements in each of the branches of an if expression, followed by a final value, that will become the value of the entire if expression, when it takes that branch:

n := arguments().len x := if n > 2 { dump(arguments()) 42 } else { println('something else') 100 } dump(x)

If unwrapping

Anywhere you can use or {}, you can also use "if unwrapping". This binds the unwrapped value of an expression to a variable when that expression is not none nor an error.

m := { 'foo': 'bar' } // handle missing keys if v := m['foo'] { println(v) // bar } else { println('not found') }
fn res() !int { return 42 } // functions that return a result type if v := res() { println(v) }
struct User { name string } arr := [User{'John'}] // if unwrapping with assignment of a variable u_name := if v := arr[0] { v.name } else { 'Unnamed' } println(u_name) // John

Type checks and casts

You can check the current type of a sum type using is and its negated form !is.

You can do it either in an if:

struct Abc { val string } struct Xyz { foo string } type Alphabet = Abc | Xyz x := Alphabet(Abc{'test'}) // sum type if x is Abc { // x is automatically cast to Abc and can be used here println(x) } if x !is Abc { println('Not Abc') }

or using match:

match x { Abc { // x is automatically cast to Abc and can be used here println(x) } Xyz { // x is automatically cast to Xyz and can be used here println(x) } }

This works also with struct fields:

struct MyStruct { x int } struct MyStruct2 { y string } type MySumType = MyStruct | MyStruct2 struct Abc { bar MySumType } x := Abc{ bar: MyStruct{123} // MyStruct will be converted to MySumType type automatically } if x.bar is MyStruct { // x.bar is automatically cast println(x.bar) } else if x.bar is MyStruct2 { new_var := x.bar as MyStruct2 // ... or you can use `as` to create a type cast an alias manually: println(new_var) } match x.bar { MyStruct { // x.bar is automatically cast println(x.bar) } else {} }

Mutable variables can change, and doing a cast would be unsafe. However, sometimes it's useful to type cast despite mutability. In such cases the developer must mark the expression with the mut keyword to tell the compiler that they know what they're doing.

It works like this:

mut x := MySumType(MyStruct{123}) if mut x is MyStruct { // x is cast to MyStruct even if it's mutable // without the mut keyword that wouldn't work println(x) } // same with match match mut x { MyStruct { // x is cast to MyStruct even if it's mutable // without the mut keyword that wouldn't work println(x) } }

Match

os := 'windows' print('V is running on ') match os { 'darwin' { println('macOS.') } 'linux' { println('Linux.') } else { println(os) } }

A match statement is a shorter way to write a sequence of if - else statements. When a matching branch is found, the following statement block will be run. The else branch will be run when no other branches match.

number := 2 s := match number { 1 { 'one' } 2 { 'two' } else { 'many' } }

A match statement can also to be used as an if - else if - else alternative:

match true { 2 > 4 { println('if') } 3 == 4 { println('else if') } 2 == 2 { println('else if2') } else { println('else') } } // 'else if2' should be printed

or as an unless alternative: unless Ruby

match false { 2 > 4 { println('if') } 3 == 4 { println('else if') } 2 == 2 { println('else if2') } else { println('else') } } // 'if' should be printed

A match expression returns the value of the final expression from the matching branch.

enum Color { red blue green } fn is_red_or_blue(c Color) bool { return match c { .red, .blue { true } // comma can be used to test multiple values .green { false } } }

A match statement can also be used to branch on the variants of an enum by using the shorthand .variant_here syntax. An else branch is not allowed when all the branches are exhaustive.

c := `v` typ := match c { `0`...`9` { 'digit' } `A`...`Z` { 'uppercase' } `a`...`z` { 'lowercase' } else { 'other' } } println(typ) // 'lowercase'

You can also use ranges as match patterns. If the value falls within the range of a branch, that branch will be executed.

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.

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

in allows to check whether an array or a map contains an element. To do the opposite, use !in.

nums := [1, 2, 3] println(1 in nums) // true println(4 !in nums) // true

[!NOTE] in checks if map contains a key, not a value.

m := { 'one': 1 'two': 2 } println('one' in m) // true println('three' !in m) // true

It's also useful for writing boolean expressions that are clearer and more compact:

enum Token { plus minus div mult } struct Parser { token Token } parser := Parser{} if parser.token == .plus || parser.token == .minus || parser.token == .div || parser.token == .mult { // ... } if parser.token in [.plus, .minus, .div, .mult] { // ... }

V optimizes such expressions, so both if statements above produce the same machine code and no arrays are created.

For loop

V has only one looping keyword: for, with several forms.

for/in

This is the most common form. You can use it with an array, map or numeric range.

Array for

numbers := [1, 2, 3, 4, 5] for num in numbers { println(num) } names := ['Sam', 'Peter'] for i, name in names { println('${i}) ${name}') // Output: 0) Sam // 1) Peter }

The for value in arr form is used for going through elements of an array. If an index is required, an alternative form for index, value in arr can be used.

Note that the value is read-only. If you need to modify the array while looping, you need to declare the element as mutable:

mut numbers := [0, 1, 2] for mut num in numbers { num++ } println(numbers) // [1, 2, 3]

When an identifier is just a single underscore, it is ignored.

Custom iterators

Types that implement a next method returning an Option can be iterated with a for loop.

struct SquareIterator { arr []int mut: idx int } fn (mut iter SquareIterator) next() ?int { if iter.idx >= iter.arr.len { return none } defer { iter.idx++ } return iter.arr[iter.idx] * iter.arr[iter.idx] } nums := [1, 2, 3, 4, 5] iter := SquareIterator{ arr: nums } for squared in iter { println(squared) }

The code above prints:

1
4
9
16
25

Map for

m := { 'one': 1 'two': 2 } for key, value in m { println('${key} -> ${value}') // Output: one -> 1 // two -> 2 }

Either key or value can be ignored by using a single underscore as the identifier.

m := { 'one': 1 'two': 2 } // iterate over keys for key, _ in m { println(key) // Output: one // two } // iterate over values for _, value in m { println(value) // Output: 1 // 2 }

Range for

// Prints '01234' for i in 0 .. 5 { print(i) }

low..high means an exclusive range, which represents all values from low up to but not including high.

[!NOTE] This exclusive range notation and zero-based indexing follow principles of logical consistency and error reduction. As Edsger W. Dijkstra outlines in 'Why Numbering Should Start at Zero' (EWD831), zero-based indexing aligns the index with the preceding elements in a sequence, simplifying handling and minimizing errors, especially with adjacent subsequences. This logical and efficient approach shapes our language design, emphasizing clarity and reducing confusion in programming.

Condition for

mut sum := 0 mut i := 0 for i <= 100 { sum += i i++ } println(sum) // "5050"

This form of the loop is similar to while loops in other languages. The loop will stop iterating once the boolean condition evaluates to false. Again, there are no parentheses surrounding the condition, and the braces are always required.

Bare for

mut num := 0 for { num += 2 if num >= 10 { break } } println(num) // "10"

The condition can be omitted, resulting in an infinite loop.

C for

for i := 0; i < 10; i += 2 { // Don't print 6 if i == 6 { continue } println(i) }

Finally, there's the traditional C style for loop. It's safer than the while form because with the latter it's easy to forget to update the counter and get stuck in an infinite loop.

Here i doesn't need to be declared with mut since it's always going to be mutable by definition.

Labelled break & continue

break and continue control the innermost for loop by default. You can also use break and continue followed by a label name to refer to an outer for loop:

outer: for i := 4; true; i++ { println(i) for { if i < 7 { continue outer } else { break outer } } }

The label must immediately precede the outer loop. The above code prints:

4
5
6
7

Defer

A defer statement defers the execution of a block of statements until the surrounding function returns.

import os fn read_log() { mut ok := false mut f := os.open('log.txt') or { panic(err) } defer { f.close() } // ... if !ok { // defer statement will be called here, the file will be closed return } // ... // defer statement will be called here, the file will be closed }

If the function returns a value the defer block is executed after the return expression is evaluated:

import os enum State { normal write_log return_error } // write log file and return number of bytes written fn write_log(s State) !int { mut f := os.create('log.txt')! defer { f.close() } if s == .write_log { // `f.close()` will be called after `f.write()` has been // executed, but before `write_log()` finally returns the // number of bytes written to `main()` return f.writeln('This is a log file') } else if s == .return_error { // the file will be closed after the `error()` function // has returned - so the error message will still report // it as open return error('nothing written; file open: ${f.is_opened}') } // the file will be closed here, too return 0 } fn main() { n := write_log(.return_error) or { println('Error: ${err}') 0 } println('${n} bytes written') }

To access the result of the function inside a defer block the $res() expression can be used. $res() is only used when a single value is returned, while on multi-return the $res(idx) is parameterized.

fn (mut app App) auth_middleware() bool { defer { if !$res() { app.response.status_code = 401 app.response.body = 'Unauthorized' } } header := app.get_header('Authorization') if header == '' { return false } return true } fn (mut app App) auth_with_user_middleware() (bool, string) { defer { if !$res(0) { app.response.status_code = 401 app.response.body = 'Unauthorized' } else { app.user = $res(1) } } header := app.get_header('Authorization') if header == '' { return false, '' } return true, 'TestUser' }

Goto

V allows unconditionally jumping to a label with goto. The label name must be contained within the same function as the goto statement. A program may goto a label outside or deeper than the current scope. goto allows jumping past variable initialization or jumping back to code that accesses memory that has already been freed, so it requires unsafe.

if x { // ... if y { unsafe { goto my_label } } // ... } my_label:

goto should be avoided, particularly when for can be used instead. Labelled break/continue can be used to break out of a nested loop, and those do not risk violating memory-safety.