07 - Control Flow

Control flow in Verge is deliberately unsurprising. The whole point of control flow is to make a program’s behaviour less surprising, not more, so nothing here should be unexpected — with the possible exception of match, which is more capable than its equivalent in many languages.

if / else if / else

Conditions don’t need parentheses, and the branches are braced blocks:

import std.io

classify: f(n: i32) => string {
	if n > 0 {
		return "positive"
	} else if n == 0 {
		return "zero"
	} else {
		return "negative"
	}
}

main: f() {
	io.writeln(classify(5))
	io.writeln(classify(0))
	io.writeln(classify(-3))
}
positive
zero
negative

Chain as many else if branches as you like; a bare else is the fallthrough.

while

while runs its body as long as the condition holds:

import std.io

main: f() {
	count := 3
	while count > 0 {
		io.writeln("tick")
		count--
	}
}
tick
tick
tick

for over collections

Iterating a slice or array binds each element in turn with for value := collection:

import std.io

main: f() {
	items := []string{"tea", "coffee", "water"}
	for item := items {
		io.writeln(item)
	}
}
tea
coffee
water

When you want the index too, bind a pair. The parentheses are optional — for i, item := and for (i, item) := are the same:

import std.io

main: f() {
	items := []string{"tea", "coffee", "water"}
	for i, item := items {
		io.writeln("{i}: {item}")
	}
}
0: tea
1: coffee
2: water

Maps iterate the same way, binding key, value:

import std.io

main: f() {
	prices := map.new<string, i32>()
	prices["tea"] = 3
	prices["coffee"] = 4
	for name, price := prices {
		io.writeln("{name} costs {price}")
	}
}
tea costs 3
coffee costs 4

(Map iteration order is unspecified, so don’t rely on it.)

Range loops

For a plain counter, iterate a numeric range. a..b is half-open (excludes b); a..=b is inclusive:

import std.io

main: f() {
	for i := 0..3 {
		io.write("{i} ")
	}
	io.writeln("")

	for i := 0..=3 {
		io.write("{i} ")
	}
	io.writeln("")
}
0 1 2 
0 1 2 3 

C-style for

When you need full control over the counter — a custom step, multiple conditions — the three-clause C-style form is there too:

import std.io

main: f() {
	for i := 0; i < 10; i += 2 {
		io.write("{i} ")
	}
	io.writeln("")
}
0 2 4 6 8 

break, continue, and return

The usual escape hatches: continue skips to the next iteration, break leaves the loop, and return leaves the whole function:

import std.io

first_even: f(values: []i32) => i32 {
	for value := values {
		if value < 0 {
			continue        # ignore negatives
		}
		if value % 2 == 0 {
			return value    # found one, leave the function
		}
	}
	return -1
}

main: f() {
	io.writeln(first_even([]i32{-4, 3, 7, 8, 10}))
}
8

break looks the way you’d expect:

import std.io

main: f() {
	for i := 0..10 {
		if i == 4 {
			break
		}
		io.write("{i} ")
	}
	io.writeln("")
}
0 1 2 3 

Pattern matching with match

match is where Verge pulls ahead of a stack of if statements. It compares a value against a series of patterns and evaluates the first arm that fits. A match is an expression — it produces a value — so it can be a function’s whole body:

Matching enums

Enum arms can destructure a variant’s payload right in the pattern:

import std.io

Color: enum {
	Red
	Green: i32
	Custom: object {
		R: u8
		G: u8
		B: u8
	}
}

describe: f(color: Color) => string {
	return match color {
		Red          => "red"
		Green(shade) => "green (shade {shade})"
		Custom(rgb)  => "rgb({rgb.R}, {rgb.G}, {rgb.B})"
	}
}

main: f() {
	io.writeln(describe(Color.Red))
	io.writeln(describe(Color.Green(7)))
	io.writeln(describe(Color.Custom(R: 255, G: 128, B: 64)))
}
red
green (shade 7)
rgb(255, 128, 64)

Matching integers and ranges

Arms can be literals, ranges, or the _ catch-all. Here the match is the function body directly, with no return:

import std.io

bucket: f(score: i32) => string {
	match score {
		0       => "zero"
		1..=10  => "low"
		11..=50 => "medium"
		_       => "high"
	}
}

main: f() {
	io.writeln(bucket(0), " ", bucket(7), " ", bucket(40), " ", bucket(99))
}
zero low medium high

Matching strings

import std.io

route: f(path: string) => string = match path {
	"/"      => "home"
	"/about" => "about"
	_        => "404"
}

main: f() {
	io.writeln(route("/"), " | ", route("/about"), " | ", route("/nope"))
}
home | about | 404

_ is the catch-all arm — the language’s polite way of saying “everything else, I guess”. The compiler checks that a match covers every case, so leaving a gap is a compile error rather than a lurking bug.

That’s the everyday toolkit. If you can read if, while, for, and match, you can read the overwhelming majority of real Verge code.