The language
Wahoo in five minutes
Everything there is to know about Wahoo, on one page: worlds, coins, questions, loops, pipes, and what each error means.
In this chapter
Wahoo is a small, statically typed language with three types, recursion, loops and block scoping. Its keywords come from the Mushroom Kingdom, but underneath it is an ordinary imperative language. Every snippet on this page can be pasted into the playground.
A first program
// Comments run from // to the end of the line.
world 1-1 {
coin lives = 3
wahoo("Mario has ", lives, " lives")
}
A program is a list of worlds and pipes, in any order. Statements end at the end of the line; there are no semicolons.
Worlds
world 1-1 { … } is where execution starts. A program may have several worlds, and they are played in order of their number: 1-1, 1-2, … 2-1, whatever their order in the file. Each world is its own level: variables declared in one are not visible in another. A bare flag leaves the current world early.
Types and values
| Type | Values | Like |
|---|---|---|
coins | whole numbers from −2147483648 to 2147483647; 1_000 is allowed | int (32-bit, wrapping on overflow) |
switch | star (true) and goomba (false) | bool |
text | "between double quotes", with \n \t \" \\ | an immutable string literal |
Texts can be stored, passed to pipes, compared with == and printed, but not joined: to print several things together, pass them all to wahoo(…).
Coins: variables
coin lives = 3 // the type (coins) is inferred
coin name: text = "Luigi" // or written explicitly
lives = lives + 1 // assignment: no `coin`
power_up lives // lives = lives + 1
power_up lives by 10 // lives = lives + 10
damage lives // lives = lives - 1
damage lives by 2 // lives = lives - 2
A coin lives until the end of the block ({ … }) where it is declared. An inner block may declare a coin with the same name, which hides the outer one until the block ends. Declaring the same name twice in one block is an error. Names start with a letter or _; a coin whose name starts with _ is never reported as unused.
Operators
From tightest to loosest binding:
| Operators | Operand types | Result |
|---|---|---|
-x | coins | coins |
* / % | coins | coins (division truncates toward zero) |
+ - | coins | coins |
< <= > >= | coins | switch |
== != | two values of the same type | switch |
not | switch | switch |
and | switch | switch (the right side only runs if needed) |
or | switch | switch (the right side only runs if needed) |
Operators of the same level group to the left (10 - 4 - 3 is 3). Comparisons cannot be chained: write a < b and b < c.
Questions
question lives > 3 {
wahoo("plenty")
} else question lives > 0 {
wahoo("careful")
} else {
wahoo("game over")
}
The condition must be a switch; braces are always required.
Loops
bounce lives > 0 { // while the condition holds
damage lives
}
run i from 1 to 5 { // i = 1, 2, 3, 4, 5 (both ends included)
wahoo(i)
}
In a run loop both bounds are computed once, before the first lap, and the counter is a new coin that exists only inside the loop. If the start is greater than the end, the body never runs.
Pipes
pipe fib(n: coins) -> coins {
question n < 2 {
flag n
}
flag fib(n - 1) + fib(n - 2)
}
pipe greet(name: text) { // no `->`: gives nothing back
wahoo("Hello, ", name)
}
A pipe is a function. Parameters and the return type must be written; flag value returns, and a pipe with a return type must reach a flag on every path. Pipes can be called before their definition and can call themselves. A pipe without a return type is called as a statement on its own line, greet("Peach"); one with a return type can also be called that way, and its value is discarded.
Printing
wahoo(a, b, …) prints its arguments one after another, with no separators, then a line break. Switches print as star and goomba. wahoo is a statement: it has no value, and no pipe may be called wahoo.
Errors and warnings
The compiler reports every problem it can find, with the line, the column, and often a hint. The code tells you which phase found it.
| Code | Phase | Meaning |
|---|---|---|
| E001–E005 | lexer | unexpected character, unclosed text, unknown escape, number too big, malformed number |
| E101–E105 | parser | something expected but not found, chained comparison, a lonely expression, else without question, too many errors |
| E201–E218 | checker | no world, duplicates, unknown names, pipes or types, type mismatches, wrong number of arguments, missing flag, misplaced flag values |
| W301–W303 | checker | warnings: unused coin, unreachable code, division by zero |
A program with warnings but no errors compiles and runs. At run time, three things stop a level early: division by zero, a division that overflows (-2147483648 / -1) and too deep a recursion. A program that loops forever is stopped after three seconds in the browser.
On the command line
The compiler is also a command-line tool, built from the same Rust code:
cargo install --path crates/wahoo-cli
wahoo run examples/fib.wahoo # compile and run (with the wasmi interpreter)
wahoo build examples/fib.wahoo # write examples/fib.wasm
wahoo wat examples/fib.wahoo # print the WebAssembly text
wahoo tokens examples/fib.wahoo # what the lexer sees
wahoo ast examples/fib.wahoo # what the parser builds
wahoo check examples/errors.wahoo --lang es