The Vela tour
Everything in the language, with examples you can paste into vela repl or a main.vela. For the precise rules see the specification.
Hello
use std/io
fn main() {
io.println("hello, world")
}
println and print are also in the prelude, so println("hi") works without the import. io additionally gives you eprintln, read_line and read_all.
Comments
// a line comment
/* a block comment
/* which nests */ correctly */
/// a doc comment: attaches to the next declaration, read by `vela doc`
Values and types
Four primitives, copied on assignment:
let n: Int = 42 // 64-bit signed
let f: Float = 3.14 // IEEE-754 double
let ok: Bool = true
let c: Byte = 'A' // 0..255
Everything else is a reference to a garbage-collected object:
let s: Str = "hello"
let xs: [Int] = [1, 2, 3]
let m: {Str: Int} = { "a": 1 }
let r: Range = 0..10
Literals:
1_000_000 0xFF 0b1010 0o777 // Int
1.0 2.5e-3 1e9 // Float
'a' '\n' '\x41' // Byte
"text" "with {interpolation}" // Str
There are no implicit numeric conversions. 1 + 1.0 is an error; write float(1) + 1.0. This is on purpose: it is the single largest source of quiet bugs in languages that allow it.
Bindings
let x = 1 // immutable, type inferred
let mut y = 2 // mutable
let z: Float = 3.0 // annotated
y = y + 1
y += 1 // also -= *= /= %=
A let always initialises, so an uninitialised variable cannot exist.
Strings
let name = "world"
println("hello, {name}!") // interpolation
println("1 + 2 = {1 + 2}") // any expression
println("a literal brace: {{ }}") // {{ and }} escape
let s = "Hello, World"
s.len() // 12
s.upper() // "HELLO, WORLD"
s.lower() // "hello, world"
s.trim()
s[0..5] // "Hello" slices are copies
s.split(", ") // ["Hello", "World"]
s.words() // splits on whitespace
s.lines()
s.replace("World", "Vela")
s.starts_with("He") // true
s.contains("lo, W") // true
s.index_of("World") // 7
"ab".repeat(3) // "ababab"
"42".to_int() // ?Int
"3.5".to_float() // ?Float
s.chars() // ["H", "e", ...] — UTF-8 aware
s.char_len() // characters, where len() counts bytes
Strings are immutable and indexed by byte. s[i] yields a Byte.
Building a string in a loop with + is quadratic, so use a builder:
let b = buf()
for x in xs {
b.push(str(x))
b.push(", ")
}
let joined = b.str()
Lists
let xs = [1, 2, 3]
let empty: [Str] = [] // an empty literal needs a type
xs[0] // 1 panics if out of range
xs[1] = 9 // assign in place
len(xs) // 3
xs[0..2] // [1, 9] a copy
xs + [4, 5] // concatenation
xs.push(4)
xs.pop() // 4
xs.insert(0, 0)
xs.remove(0)
xs.contains(9)
xs.index_of(9) // -1 when absent
xs.first() // ?T
xs.last() // ?T
xs.reverse()
xs.copy()
xs.is_empty()
xs.map(|v| v * 2)
xs.filter(|v| v > 1)
xs.fold(0, |a, b| a + b)
xs.each(|v| println(str(v)))
xs.any(|v| v > 2)
xs.all(|v| v > 0)
xs.find(|v| v % 2 == 0) // ?T
xs.sort() // in place, needs `<` on T
xs.sort_by(|a, b| a < b) // in place, O(n log n), no allocation
xs.join(", ")
Maps
let m = { "a": 1, "b": 2 }
let e: {Str: Int} = {:} // empty
m["a"] // ?Int — lookup yields an optional
m["a"] ?? 0 // 1
m["z"] ?? 0 // 0
m["c"] = 3 // insert or update
m.has("a")
m.delete("a")
m.len()
m.keys() // [Str], unspecified order
m.values()
m.get_or("z", -1)
for k, v in m {
println("{k} = {v}")
}
Keys may be any type with structural equality: Int, Str, Byte, Bool, Float, enums, structs and lists of those.
Control flow
if x > 0 {
println("positive")
} else if x == 0 {
println("zero")
} else {
println("negative")
}
while i < n {
if skip { continue }
if done { break }
i += 1
}
for i in 0..n { } // exclusive
for i in 0..=n { } // inclusive
for x in xs { }
for i, x in xs { } // index and value
for k, v in m { }
for b in "abc" { } // bytes
Braces are always required.
if as an expression
An if with an else produces a value, and a block's final expression is that block's value:
let label = if n < 0 { "negative" } else if n == 0 { "zero" } else { "positive" }
let cost = if premium {
let base = 100
base * 2
} else {
50
}
Functions
fn add(a: Int, b: Int) -> Int {
return a + b
}
fn greet(name: Str) { // no `->` means it returns nothing
println("hi, {name}")
}
Parameter and return types are always written out. Inside a function body everything else is inferred.
Methods
struct Point {
x: Float,
y: Float,
}
fn Point.length(self) -> Float {
return (self.x * self.x + self.y * self.y).sqrt()
}
fn Point.scale(self, k: Float) {
self.x = self.x * k
self.y = self.y * k
}
Methods can be declared on any type in the same module and on the built-in types — which is exactly how the standard library provides Str.trim and List[T].map. There is no impl block and no inheritance.
Closures
let double = |x| x * 2 // types inferred from context
let add = |a: Int, b: Int| a + b // or written out
let sum = |xs: [Int]| { // a block body: last expression wins
let mut t = 0
for x in xs { t += x }
t
}
fn make_counter() -> fn() -> Int {
let mut n = 0
return || { n += 1; n } // captures `n` by reference
}
A captured variable is shared with its enclosing scope and outlives the frame.
Structs
struct User {
name: Str,
age: Int,
tags: [Str],
}
let u = User{ name: "ada", age: 36, tags: [] } // every field is required
u.age += 1
println("{u}") // User{name: ada, age: 37, tags: []}
println("{u == u}") // structural equality, recursive
Structs are references: assigning one does not copy it.
struct Pair[A, B] {
first: A,
second: B,
}
let p = Pair[Int, Str]{ first: 1, second: "one" }
Enums
enum Color { Red, Green, Blue } // no payloads: a plain integer
enum Shape {
Circle(Float),
Rect(Float, Float),
Empty,
}
let c = Color.Green
let s = Shape.Rect(3.0, 4.0)
Generic enums work too:
enum Tree[T] {
Leaf,
Node(Tree[T], T, Tree[T]),
}
Pattern matching
match is both a statement and an expression.
let area = match shape {
Shape.Circle(r) => 3.14159 * r * r,
Shape.Rect(w, h) => w * h,
Shape.Empty => 0.0,
}
let size = match n {
0 => "none",
1 | 2 | 3 => "a few",
_ if n < 100 => "some",
_ => "many",
}
match point {
Point{ x: 0, y: 0 } => println("origin"),
Point{ x: a, y: b } => println("at {a},{b}"),
}
match maybe {
nil => println("missing"),
some(v) => println("got {v}"),
}
match result {
ok(v) => println("ok {v}"),
err(e) => println("failed: {e.msg}"),
}
Arms take an expression or a block; a block's final expression is the arm's value. A match used as an expression must be exhaustive, and the compiler tells you which cases you are missing.
Optionals
let a: ?Int = nil
let b: ?Int = 5 // a plain value widens automatically
if let v = b {
println("got {v}")
} else {
println("nothing")
}
let c = b ?? 0 // fallback
let d = b? // propagate nil out of a ?-returning function
?T and T are different types, so a missing value cannot be used by accident.
Errors
Errors are values. !T is a T or an Error { msg: Str, code: Int }.
fn parse_port(s: Str) -> !Int {
let n = s.to_int() ?? return err("not a number: {s}")
if n < 1 or n > 65535 {
return err("port out of range: {n}")
}
return ok(n)
}
fn main() -> !Void {
let p = parse_port("8080")? // propagates the error
let q = parse_port("bad") ?? 80 // or supplies a default
println("{p} {q}")
match parse_port("x") {
ok(v) => println("ok {v}"),
err(e) => println("error: {e.msg} (code {e.code})"),
}
return ok(void)
}
main may return nothing, an Int exit code, or !Void — in which case an error is printed to stderr and the process exits non-zero.
There are no exceptions and no stack unwinding. panic("...") exists for genuinely unrecoverable states; it prints and exits 101.
Generics
fn largest[T](xs: [T]) -> ?T {
if len(xs) == 0 {
return nil
}
let mut best = xs[0]
for x in xs {
if x > best {
best = x
}
}
return best
}
largest([3, 1, 2]) // ?Int
largest(["b", "a"]) // ?Str
largest[Float]([1.5]) // explicit type argument
Generics are monomorphised and there are no trait bounds. If the body needs > and the type has >, it compiles; if not, you get an error at the instantiation site telling you which operation is missing and where the instantiation was.
Modules
One file is one module. The last path segment is the binding name.
use std/io // -> io
use std/fs
use std/str as text // rebind
use ./util // a sibling file util.vela
use ./sub/thing // sub/thing.vela
use json/parse // module `parse` of the dependency `json`
pub exports a declaration; without it, it is private to its module.
pub const VERSION: Str = "1.0"
pub struct Config { path: Str }
pub fn load() -> !Config { ... }
fn helper() { } // private
Tests
test "addition works" {
assert(1 + 1 == 2)
assert_eq(2 + 2, 4)
assert_ne("a", "b")
}
test "errors propagate" {
let v = might_fail()? // `?` works: a test body is a `!Void`
assert_eq(v, 42)
}
vela test compiles every test block in the project into one binary and runs it. A failed assertion or a propagated error fails that test.
Type aliases
type Grid = [[Int]]
type Handler = fn(Str) -> !Str
Aliases are transparent — the same type, under another name.
Intrinsics
For the standard library only. These are how core talks to the machine.
@syscall(n, a1..a6) @load8/16/32/64(addr) @store8/16/32/64(addr, v)
@addr(x) @ref[T](addr) @sizeof[T]()
@f2bits(f) @bits2f(i) @fsqrt(f)
@stack_top() @rt_base() @argc() @argv() @envp()
@save_regs() @restore_regs() @trap()
If you find yourself reaching for one of these in application code, the standard library is probably missing something.