Chapter 5 - Statements
Statements manage the flow of code execution.
BlockStatement
We’ve seen the block statement several times already. It’s attached to functions we write, foreach
, if
, and many others. They group other statements together, and introduce a new scope. Inside a function, we can just add a block statement on its own, if we want the ability to use the same variable name for different variables. If you find yourself doing this, it’s probably a sign to split your function into more functions, but it’s there if you need it.
import watt.io;
fn main() i32
{
{
x := 2;
writeln(x);
}
{
x := "hello";
writeln(x);
}
return 0;
}
Output:
2
hello
Return Statement
We covered the return
statement briefly; it stops execution of a function and returns a value to the caller. If a function has a void return value, you can use return;
to return execution from that function.
import watt.io;
fn foo(n: i32)
{
if (n < 100) {
return;
}
writeln("big n");
}
fn main() i32
{
foo(101);
foo(50);
return 0;
}
Output:
big n
Import Statement
We touched on import
statements earlier in chapter 3, but there’s more to them than import watt.io
.
At the top of this chapter we said that statements affect flow of execution. At first blush, this doesn’t seem to be true of import
statements; they don’t do anything on their own, do they?
In fact, an import
statement modifies every single lookup of an identifier in the module. ‘Import’ is perhaps a slight misnomer, then, as what the import
statement does is add a scope (a place where names are stored) to the places the compiler looks when you type a name that isn’t a keyword or builtin type.
Simple Import
This is what we’ve been using.
import mymodule;
Any public
symbol that has a name that matches a lookup will be retrieved, unless a local declaration (one in the same module) exists by that name. Local declarations always trump something retrieved by import. In this way, importing code won’t silently change behaviour.
Alias Import
An alias import
is like a simple one, but gives a list of symbols.
import mymodule : a, b;
Only symbols included in the list after the colon will be considered. The aliases can also be renamed.
import mymodule : newName = oldName;
The declaration mymodule.oldName
is accessible, but only through newName
. This is helpful to avoid name collisions, or simply to make code prettier.
Bind Import
import bindname = mymodule;
Now to use something inside of mymodule
, you have to go through bindname
:
bindname.someFunction(12);
You can associate multiple modules with a single bindname by wrapping a list in [
and ]
:
import bindname = [modulea, moduleb];
Liberal use of bind imports is recommended, as it keeps dependencies obvious, code clean, and makes detecting unused import
s very easy.
Public Import
By default, import
statements are private
– they only affect the module with the import statement. If you prepend an import
statement with public
, to anyone importing a module, it will be as if they also imported all of the public imports too.
public import mymodule;
Assert Statement
The assert
statement verifies a condition. If the condition is false, the execution of the program is halted. An optional message of your own can be supplied to be displayed. Use this for things that should not happen; they help point out bugs earlier. In general, the earlier a bug is found, the easier it is to fix. So use them liberally. Depending on the compiler, these may or may not be included in release builds, so never rely on them for error handling.
fn main() i32
{
assert(true, "this assert will not trigger");
assert(5 > 10, "but this one will");
return 0;
}
While Statement
Like a foreach
statement, while
executes code in a loop. Like an if
statement, it only evaluates one condition.
import watt.io;
fn main() i32
{
i := 0;
while (i < 3) {
writeln(i++);
}
return 0;
}
Output:
0
1
2
Do While Statement
This is like a regular while
statement, except the condition is checked at the end of a loop. So even a loop with a false condition is executed once. Not used a lot, but handy when you need it.
import watt.io;
fn main() i32
{
do {
writeln("A");
} while (false);
return 0;
}
Output:
A
For Statement
A for
statement is another loop. It looks a little complicated at first, but you’ll get used to it.
for (<variables>; <condition>; <iterator>) {
}
The variable is declared for the entirety of the for
loop’s block statement, the condition is checked to see if the loop is continued, just like a while
loop. And the iterator is run after each loop.
import watt.io;
fn main() i32
{
for (i := 0; i < 5; ++i) {
writeln(i);
}
return 0;
}
Output:
0
1
2
3
4
All of the sections are optional. If the condition is empty, it is considered to always be true. So a common way of writing an infinite loop (a loop that never terminates) is then:
for (;;) {
}
Implicit Casts To Bool In If, For, While, and Do While Statements.
The if
, for
, while
and do .. while
statements all share a common property: they each have an expression that dictates how they behave, depending on if that expression is true
or false
. To aid in code brevity, the implicit boolean cast behaviour in these expression differs to regular implicit rules.
These conversions fall into three broad categories.
Primitive types.
An i32
, for example. The rules here are simple. A value of 0
is considered false
, anything else is considered true
.
if (integer) { ... }
Becomes equivalent to:
if (integer != 0) { ... }
Pointer and reference types.
Any kind of pointer, or an instance of a class
. Again, the rules are simple. A value of null
is considered false
, anything else is considered true
.
if (ptr) { ... }
Becomes equivalent to:
if (ptr !is null) { ... }
Arrays
If an array’s length
parameter is greater than 0
, it is considered true
. Otherwise, it is considered false
. The value of ptr
is entirely irrelevant to whether or not the array is considered true
or false
.
if (array) { ... }
Becomes equivalent to:
if (array.length > 0) { ... }
Other
Anything else not mentioned above (other than bool
expressions) will generated an error if used uncased in one of the afore mentioned statements.
Foreach Statement
We’ve been introduced to the foreach
statement. In general:
foreach (indexVariable, iterationVariable; whatWeIterateOver) {
}
For arrays, the indexVariable
is a size_t
that contains the number of times the loop has been completed; 0
the first time, 1
the second time, and so on. The iterationVariable
will be a variable of the base type of the array being iterated over. If it’s a variable of type i32[]
, then the iteration variable will be of type i32
.
Iterating over an associative array has the same form, but the variables take on slightly different meanings. The indexVariable
will contain a key, and the iterationVariable
will contain the value associated with that key.
aa: bool[string];
aa["apple"] = true;
aa["banana"] = false;
foreach (key, value; aa) {
writeln(new "'${key}':${value}");
}
Output (the order may differ on different OSs and runtimes):
'apple':true
'banana':false
Here’s a different form that is often useful:
import watt.io;
fn main() i32
{
foreach (i; 0 .. 5) {
writeln(i);
}
return 0;
}
Output:
0
1
2
3
4
If you don’t need the value of the range, and just need a loop to iterate an arbitrary amount of times, you can omit the iteration variable entirely:
foreach (0 .. 10) {
// run 10 times
}
If you declare the iteration variable as ref
, if you modifiy it, you’ll modify what you’re iterating over.
import watt.io;
fn main() i32
{
a := [2];
foreach (ref i; a) {
i *= 3;
}
writeln(a[0]);
return 0;
}
Output:
6
Reverse Foreach
foreach_reverse
is like a regular foreach
, except it goes from the end of a list to the beginning.
import watt.io;
fn main() i32
{
a := [1, 2, 3];
foreach_reverse (i; a) {
writeln(i);
}
return 0;
}
Output:
3
2
1
Foreach Over Strings
“If you were to use the foreach
statement to iterate over a string
, what would happen?” To the novice, this may seem like a simple question. When we use foreach
on, say, an i32[]
variable, the element type is i32
, and it goes over each element in the list one by one, with the index increasing by one every iteration. A string
is an alias for immutable(char)[]
, so foreach must use an element type of char
, going over each character in the string. Right?
If string
meant ‘ASCII’, you’d be right. However, a string
is not ‘ASCII’. In Volt, the string type is encoded as UTF-8. The readers that don’t know what UTF-8 is, or what Unicode is, are strongly encouraged to read Joel Spolsky’s classic article, The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets. If you work with computers in any capacity, the knowledge will be valuable to you.
Given the following piece of code:
str: string = "Hello, world.";
foreach (c; str) {
writefln("%s", c);
}
The question becomes: what do we do? Do we treat it like any other array, byte 0 = 'H'
, byte 1 = 'e'
, etc? In a lot of cases, that would seem to be the “obvious thing”, but consider the following:
str: string = "こんにちは、世界";
foreach (c; str) {
writefln("%s", c)
}
The Japanese kana and kanji of the above string are represented by three bytes each in UTF-8, so simple iteration wouldn’t work. Volt’s runtime has code for determing how much a given index has to be increased to get to the next character. So another option is to call those functions automatically. Works for both examples, problem solved, right?
Not quite. Firstly there’s an issue of philosophy: Volt likes to avoid hidden code wherever possible, and not everyone would be expecting that behaviour. More practically, there are cases where programs would want to iterate over a UTF-8 string byte by byte, and forcing a cast(u8[])
in this case isn’t ideal.
The solution Volt has opted for is to make you choose explicitly what behaviour you want. The above two code examples will generate an error, prompting you to explicitly choose the type of the iteration variable (that is, c
).
foreach (c: char; str) - simple iteration
foreach (c: dchar; str) - decode a UTF-8 character
foreach_reverse (c: char; str) - simple backwards iteration
foreach_reverse (c: dchar; str) - decode a UTF-8 character, starting from the last and going backwards.
Switch Statement
A switch
statement runs code depending on its value. Similar to several if
else if
chains, but more compact.
import watt.io;
fn main() i32
{
a := 3;
switch (a) {
case 1, 2:
writeln("one or two");
break;
case 3:
writeln("three");
break;
default:
writeln("default");
break;
}
return 0;
}
Output:
three
If the break
isn’t present in a non-empty case, the compiler will error. This is because in C, a switch statement would ‘fall-through’ to the next case if break
was not present. If you wish to jump to the next case, use goto case;
instead of break;
. An empty case is special cased to fall-through quietly.
case 1:
case 2:
case 3:
// This hits cases 1 and 2 as well.
Continue Statement
The continue statement can only occur inside of a loop. When it is run, the current loop starts from the beginning, if its condition is true. Otherwise, the loop is exited.
import watt.io;
fn main() i32
{
i := 0;
while (i < 10) {
if (++i <= 9) {
continue;
}
writeln(i);
}
return 0;
}
Output:
10
Break Statement
We’ve seen the break
statement breaking out of switch
case
s, but it works in loops too. It breaks out of the current loop, regardless of the its condition.
import watt.io;
fn main() i32
{
do {
writeln("A");
break;
} while (true);
return 0;
}
Output:
A
With Statement
The with
statement takes an expression that is checked first when doing lookups. This can make some code easier to read.
arr := [1, 2, 3];
with (arr) {
return length; // Same as arr.length.
}
Synchronized Statement
The synchronized
statement ensures that only one thread can execute in its block. If that’s meaningless to you, it’ll make more sense once you learn about threads.
synchronized {
foo();
}
Throw Statement
The throw
statement takes an instance of any object inheriting from core.exception.Throwable
. The stack is then ‘unwound’ (control is passed back to the function that called the current function) until it finds a catch
statement that matches, or if it can’t, the Volt runtime will kill the entire process. This is for errors that are expected to happen. For instance, if a file cannot be opened, the file opening function might throw a FileNotFoundException
.
Try/Catch/Finally
If an exception is thrown by code in the try
statement’s block, its attached catch
statements are checked to see if they match the thrown Exception, and if they do, control is passed to them. If there is a finally
block attached to the try
statement, it receives control last, regardless what exception was caught.
import watt.io;
import core.exception;
fn main() i32
{
try {
throw new Exception("");
} catch (e: Exception) {
writeln("caught!");
} finally {
writeln("finally!");
}
return 0;
}
Output:
caught!
finally!
Scope Statement
The scope
statement is executed when a function is left. scope (success)
is run when a function is exited via a return
statement. scope (failure)
is run when a function is exited via a throw
statement. And scope (exit)
is always run, no matter how the function was left.
import watt.io;
fn main() i32
{
scope (exit) {
writeln("bye!");
}
writeln("hi!");
return 0;
}
Output:
hi!
bye!