Vela.

A minimal, safe, fast systems language with a fresh face.

What is Vela?

Vela is a small systems programming language designed to be minimal like Go, strictly typed with exhaustive matching, and expressive like modern functional languages — without the bloat. It has one clear way to do things: one kind of function, one kind of record, one kind of sum type, and memory managed through regions, without a garbage collector.

Vela compiles to C through a Rust-based compiler, so it runs anywhere C runs and inherits its performance while keeping the source language tidy and opinionated.

Philosophy

Hello, Vela

; A tiny Vela program
task main()
    show("Hello, Vela!")
fi

A program is a .vela file, with any files it includes (see Modules). Compile and run it with (the output is a.out unless -o names it):

cargo run -- examples/hello.vela -o examples/hello
./examples/hello

More programs are in examples/: fib.vela (recursion), receipt.vela (optional arguments, a lambda among them), pantry.vela (new without ( )), kinds.vela (kinds, match, and a kind's methods), methods.vela (a shape's methods, alters, priv, a method that can fail, one of a shape that takes types, fields that name the value they fall back on, a read field, and a shared one), todo.vela (a linked list in a region, with maybe pointers, is, and a plan B), errors.vela (T ~ E, fail and failure, and handling errors with match, is joined with and, and a plan B, one giving none too), text.vela (lengths, parts, order, joining, values as text, and text holding values), regions.vela (a tree in a region, a region per loop pass, copying a result into a longer-lived region, and the region named after a call), arrays.vela (arrays, and slices in a region: sorting and a sieve of primes), shop.vela (a program in three files, with include, braces, =>, and priv), files.vela (a list kept in a file with std/files: written, added to, read line by line, and deleted), numbers.vela (numbers read from input with scan("> ", int), and from text with int(t), with the reasons there is no number), generics.vela (tasks that take types, and the words that say what a type allows), tuples.vela (tuples made, read, and taken apart), maps.vela (words counted in a map), primes.vela (a slice filled by index and one grown with add, emptied with del, and a loop through a slice as it is at each pass), stock.vela (a stock list searched with kinds that take types), kind_imitation.vela (orders refused for reasons of two kinds, one imitating the other), patterns.vela (a till ringing up books and lamps, which follow a pattern), pipes.vela (orders read, reserved, and priced in a chain of pipes, which one plan B ends), seats.vela (a party seated in a hall, with break, continue, and a named loop), collatz.vela (Collatz walks counted with a loop in three parts and endless), readings.vela (sensor readings searched with loops that give a value and LoopResult), journal.vela (notes read from input into a region per line, with a default region and copy()), and demo.vela (tasks, shapes, pointers, control flow, and text).

Prefer an editor? Vela Studio highlights Vela, shows errors as you type, completes names, and runs the file with F5: vela-studio examples.

Types

Syntax Overview

Every block — a task, shape, kind, when, loop, each, or match — closes with fi. Write each statement on its own line, with nothing after it but a comment. A ; starts a comment, as a semicolon adds a remark to a sentence (fixed x = 1 ; the first), and a # separates an array's type or value from its count, as in item #4: [int#4], [0#4]; first inside an index, it asks for the item safely, as a maybe: xs[#i] (see Safe index). A # comment is an error that says so, and a count written with ;, [0; 4], is an error that says a count follows #. Everything after a ; is a comment, wherever it is but inside text or a char (';'), so show(1); show(2) is only show(1): Vela has no symbol that separates statements. (The compiler doesn't insist on the line break yet; see Current Limits.) Files may use LF or CRLF line endings; a carriage return alone (as old Mac files end lines) is an error.

A statement is a binding, an assignment, a block, a send (<-), a result update (total <- value, see Named Results), a region, or a task call, which may have a plan B (see Maybe and Errors). Any other expression on its own line would do nothing, so it is an error: x == 1 alone reports the result of '==' is unused; use '=' to assign. A when and a match are blocks, and so statements, but they are also values where one is expected (see A match or a when as a value). Comments start with ;.

Reserved Words and Symbols

These words can't be used as names:

task shape kind  bind hold  when   else loop    each
in   at    match case new   region and  or      not
true false none  fi   maybe is     fail failure include priv
block shared type break continue endless fixed flex
finally

A variable is declared with one of four words: fixed (immutable binding), flex (mutable place and contents), bind (place cannot be reassigned, contents may change), or hold (place can be reassigned as a whole, contents cannot be changed).

Variable initialization can use = for ordinary values or := as a shortcut to allocate in the block's active default region. := is valid only on declarations (never on reassignment). It accepts unambiguous slice literals (flex s := [1, 2, 3]) and map literals (flex m := ["a": 1]), or explicit pointer type annotations (bind x: ^int := 4). Ambiguous scalar assignments without a pointer type are rejected at compile time.

show, scan, and the builtin types (int, float, bool, unit, char, text, nothing, and the sized numbers) aren't reserved words: no task, shape, kind, or variant can take their names, but a variable, parameter, or field can (see Names). The builtin kinds ParseError (NotANumber, TooLarge) and InputError (Ended, NoNumber) take their names the same way, and, as for any kind, a variable may not be named after a kind or one of its variants; a field may. Nor may a variable be named after a shape, whose shared fields Shape.f names. new is reserved everywhere but after shared and after a dot, where it names a shared method, Point.new(…). Seven words are read by their position and are no more reserved than those: alters before a method (see Methods), alias and pattern at the start of a line outside every item (see Type aliases and Patterns), read before a shape's field (see Read-only fields), default after a region (see A block's default region), this, and native before a task of the standard library (see How the library is made). type is reserved everywhere and read only after a dot, as x.type. The symbols:

Symbol Meaning
-> T the type a task sends back, in its header (several: -> int, int; named: -> total: int)
<- value sends that value back (starts a line; several: <- a, b)
<- ends the task, sending its named results as they are
name <- value sets a named result and keeps going (several: steps, peak <- 0, start)
name: T a parameter's, field's, or binding's type
T ~ E a type with an error side: a T when it works, an error E when it fails (see Maybe and Errors)
^T, p^ a pointer type; the value a pointer points to
T@r, new@r the region a type's pointers live in; the region new allocates in (also new T(v) at r)
value { … } a plan B: the value inside a maybe or T ~ E, or else what the block gives
a |> f(b), a |>> f(b) a pipe: the call f(a, b), or f(b, a), each > one argument further; a step that fails stops the chain (see Call pipes)
xs.map(…), it.filter(…), it.collect(r) a lazy iterator chain off a slice or an iterator, in a file that includes std/collections; nothing is gathered until a consumer asks (see Lazy iterators)
a + b, -a, a < b on one shape the operators a type's pattern names by method name (plus, minus, times, div, rem, neg, cmp), both operands the one type (see Operators a pattern names)
{v}, _ in a case or is: the value inside, and anything (or a value left unnamed); _ also drops a result
a.x a field of a shape value
a.m(…), alters m(…) a call of a method of the value's shape or kind; a method that changes its value (see Methods)
.x, .m(…), .this, @this inside a method: a field of the value it is called on, another of its methods, the value itself, and the region it lives in
; … a comment, to the end of the line
[T#n], [T]@r an array type; a slice type (see Arrays and Slices)
width: int = 8 a parameter a call may leave out, one of the last, which is then given the value written (see Optional arguments)
flex x: T, flex& x: T, f(&a) a parameter the task changes: its own copy, or the caller's place, which the call marks with & (see Parameters that change)
fixed x: T, fixed& x: T a parameter that is always a copy, or the caller's place only read, given as &a (see Parameters that read)
bind x: T, bind& x: T a parameter whose contents the task changes, never assigning it as a whole: its own copy, or the caller's place (see Parameters that bind)
flex& y = &x, fixed& y = &x, bind& y = &x another name for the place x, which changes it, only reads it, or changes its contents (see References)
show(&x), &a == &b where the place x is, as an address; whether two places are one (see Addresses)
kind Option<T>, Option<int>.None a kind that takes types, and a variant given them where nothing else says them (see Kinds that take types)
kind OrderError::ReadError, kind A::B & C a kind that imitates another, or several: it has their variants before its own, and a value of one goes where it is expected (see Kinds that imitate kinds)
each v in &xs the items of the place xs as they are at each pass (see Arrays)
loop flex i = 0 || i < n || i++, endless a loop in three parts, its start, condition and step, each with several items parted by , or none; a loop that never ends by itself (see A loop in three parts)
break, continue, outer => each …, break outer stop a loop, or end its pass and go on with the next; a loop named so, which a loop inside it stops or goes on with (see Stopping a loop)
fixed r = each x in xs … finally v fi, break = v a loop that gives a value, via finally or map-form, or fails with LoopResult; a break that stops it with one (see Loops that give a value)
<T>, <T: shown> the types a task, shape or kind takes, and what one must allow; & joins two words (see Types a declaration takes)
first<int>(xs), Box<int>, Box<int> { … } the types a use gives: a call, a written type, a shape literal
(T, U), (a, b), t.0 a tuple type; a tuple of the values; its first item (see Tuples). One type or value in parentheses is only grouped: (int) is int
(int, text) -> int, (flex& int, @r) -> text@r, square, f(x) a task's type; a task named as a value, without (); the call of the task a value holds (see Tasks as values)
$d, $d(values) a startup decorator, above a task or a shared method: runs d(task, values) once, before main (see Decorators)
$(.checked) above a method of every value: a method of the same type wrapping it, called on the same value (see Wrappers on methods)
<P...>, args: P..., (P...) -> R, (@a, P...) -> R, f(args...) a group of types a task takes; the values a call gives for it; a task taking them; one taking a region, then them; the values given on, whole (see Type groups)
$(w1, w2(values)) wrapper decorators, above a task, a shared method, or a method of every value: each call of it runs w1, given the rest of the chain, its values, and the task's (see Decorators)
(x: int) -> int <- x * 2 fi a lambda: a task with no name, written where its value goes, its body on the header's line or the lines after it (see Lambdas)
x -> x * 2, (a, b) -> a + b, () -> 42 a one-line lambda, its types from the place it goes (see One-line lambdas)
[@r](x: int) -> int … fi, [@r](int) -> int a lambda that keeps a copy of what it uses of the code around it in region r, and its type (see Lambdas that keep what they use)
[flex @r](…) … fi, [bind @r](…) … fi such a lambda that changes its copies, held in a flex variable and never copied, or only what its copies share, copied freely
[flex& count, fixed& big, @r](…) … fi such a lambda keeping variables themselves, to change or to read in place (or bind& xs, to change their contents), with r declared after them (see Lambdas that keep what they use)
[a, b], [v#n] an array of the values; an array of n copies of v
[K: V]@r, [:V]@r, ["a": 1] at r a map type; one whose keys are text; a map written by its pairs, as new [text: int](["a": 1]) at r (see Maps)
m[k], m[#k], m[k] = v, each k, v in m a key's value, which stops the program without the key; the same as a maybe; the key added or given a new value; the keys and values in the order the keys were added
[a, b, ...] at r a slice of the values in region r, as new [T]([a, b]) at r (see Written by its values)
xs[i], xs[a..b], xs.len an item of an array or slice; a slice of it; how many items it has
t.#k is maybe int {x}, t.#k? is maybe int a test of the type of a tuple's item, which the compiler knows; ? asks for the type itself (see Tuples)
xs[#i] the item at i of an array, slice, or text in a maybe, none out of range (see Safe index)
t[a..b], t[i], t.at(i), t.len a part of text, the same bytes (see Parts of text); the byte at i as a char, and as a maybe char; how many bytes it has
'a' a char, one byte (see Chars)
a + b at r, (a + b)@r text joined into new text in region r (see Joining text)
f(x) at r, f(x)@r the region a task or method takes last, given after the call: at r covers everything before it (a whole + chain), @r only the call right before it (see The region after a call)
x.str() at r, x.str(r), text(x) at r a value as text in region r (see Values as text)
x.json() at r, x.json(r), to_json(x, r) a value as JSON text in region r (see Values as JSON)
"n = {n}" at r, "n = {n}"@r text holding values, in region r; \{ and \} write braces (see Text holding values)
xs.add(v), xs.add(i, v), xs.del(i), xs.del() add a value to a slice, last or before item i; take item i, or the last, out of it and give it
xs.added(v) at r, xs.without(i) at r, m.added(k, v), m.without(k) a new slice or map with one change, a complete copy, leaving the original as it was (see A new slice with one change)
x.copy(), x.copy() at r a copy of a value in the region where it goes, or in r (also x.copy()@r; see Copying into another region)
#NAME = value a macro: a name for a value the compiler computes, at the start of a line outside every task (see Macros)
alias Name => type a type alias: another name for a type, at the start of a line outside every task (see Type aliases)
pattern Shaped … fi, shape Circle::Shaped, <T: Shaped> a pattern, the fields and methods a shape or kind following it has, some with default bodies; a shape following it; a type a task takes that follows it (see Patterns)
pattern Iterator<T>, shape Countdown::Iterator<int>, <I: Iterator<T>, T> a pattern that takes a type; a shape giving it one; a type a task takes that follows it, T worked out from what I follows (see Types a pattern takes)
apply<R>(f: (T) -> R) -> R a method taking a type of its own, told by a call's values (see Types of its own)
m.name, a/b a task, shape, kind, type alias, variant, or macro of module m; a folder in an include path (see Modules)
Kind.Variant a variant, named through its kind (see Kinds)
Shape.f, Shape.m(…), Kind.m(…), Shape.m a shared field of a shape, and a shared method of a shape or kind, named through it, called or as a value (see Shared fields and shared methods)
x.type the type of x, as text (see The type of a value)
name => other on an include line: the name a module or a name brought in is written with in this file; before a loop's word, the loop's name (see Stopping a loop)
Name { x: 1 } a shape literal
0..5 a range, in each and in a slice (xs[1..3])
= assignment, the value of a binding, the value a field or a parameter falls back on, and the value a case gives
+ - * / %, == != < <= > >= arithmetic and comparison (see Comparing)
x++, x--, x += v a place counted by one, or given its operator's result with v (also -= *= /= %= **=); statements of their own (see Steps)
a ** b a raised to the power b, before *, grouping from the right (see Powers)
a ~= b, a <> b whether two values are similar, item by item; not (a ~= b)

Tasks

Functions are called tasks. -> T in the header says a task sends back a value of type T, and <- sends it: one arrow points out of the header to the result, the other brings the result back. A task that sends nothing back has no arrow in its header.

task add(a: int, b: int) -> int
    <- a + b
fi

task greet(name: text)
    show(name)
fi

A task that returns a value must send one on every path: the compiler rejects a task that could reach its end without sending. A when counts only if both branches send, a match only if every case does, and a loop counts only if it never ends by itself and no break stops it, as an endless one that leaves only with <- (any other loop's body might not run). A task without a return type uses a bare <- to return early. A task whose results are named sets them instead, and reaching its end sends them; the same paths are walked, to make sure each one is set (see Named Results).

task sign(x: int) -> int
    when x < 0
        <- -1
    fi
    <- 1                 ; without this line: task 'sign' can finish without sending a value
fi

task report(x: int)
    when x == 0
        <-                  ; nothing to send: just leave
    fi
    show(x)
fi

A send starts its line. <- is always one symbol, so a comparison with a negative number needs a space: a < -1 compares, and a <-1 is an error. In the middle of a line the error says so — '<-' sends a value and must start a line; to compare with a negative number, write '< -' — and at the start of a line, where name <- value sets a named result, it reports that a isn't a result of the task.

Tasks can be called before they are declared, and can call themselves. Arguments are copies (a large one is passed faster, without a difference a program can see but its address: see large values), so a task never changes the caller's value; a parameter never changes either, unless it is written flex, and a task changes the caller's own value only through a parameter written flex& (see Parameters that change) or a pointer.

Every program starts at task main(), which takes no parameters and returns nothing.

Optional arguments

The last parameters of a task may name a value to fall back on, written after the type: width: int = 8. A call that leaves them out is given those values, so pad("a") reads as pad("a", 8, '.') below. Only the last ones can: a parameter without a value can't follow one with a value, so a call leaves out a tail of the list and never a value between two it gives. The same goes for methods and shared methods, and for the values a startup decorator takes after the task, so $d may leave them out too.

task pad(t: text, width: int = 8, fill: char = '.')
    show(width, fill, "|", t)
fi

task apply(x: int, f: (int) -> int = (n: int) -> int <- n + 1 fi) -> int
    <- f(x)
fi

pad("a")              ; 8.|a
pad("b", 3)           ; 3.|b
pad("c", 3, '-')      ; 3-|c
show(apply(4))        ; 5
pad()                 ; error: task 'pad' expects 1 to 3 arguments, got 0

The value is one the compiler works out without running the program, as a field's is: a number, text, true, false, none, a variant, arithmetic on numbers and macros (worked out once), a tuple, an array, or a literal of those, and, for a parameter of a task's type, a task named as a value or a lambda that keeps nothing. So it never reads another parameter, calls nothing, and allocates nothing, and the call that leaves it out is the same program as the call that writes it: the C function takes every value, and the compiler writes the missing ones into the call. The value belongs to the task's file, which reads it wherever the call is, so it may name what only that file sees, such as a private macro or task.

Some parameters can't have one: a place, flex& or fixed&, which the caller names with &; a region, which the call names; a parameter of a type the task takes (x: T), a different type in every use, except fallback: maybe T = none, since none fits every maybe; and every parameter of a task that takes a group, args: P..., whose values after the others would be the group's or these (a group can be given none already). A lambda's own parameters have none, nor does a lambda that keeps what it uses make one, since the region it keeps them in is one no default can name. flex n: int = 0, the task's own copy, can.

The values belong to the task's name, not to its type: a task named as a value, fixed f = pad, is a (text, int, char) -> nothing, and a call through it gives every value. A call of a task with wrappers is given the values first, so the chain gets them all; in $(w(…)), a wrapper's own values come before the task's and are all written. Arguments are still matched by their place: there are no named arguments to leave out a value in the middle.

Parameters that change

A parameter is immutable: n = n + 1 in a task that takes n: int reports cannot assign to immutable variable 'n'; a parameter that changes is written 'flex n: int', and is the task's own copy. Written flex before its name, a parameter is the task's own copy of what it was given, which it changes as it changes any flex variable: assigned, its fields and items assigned, its alters methods called, a slice added to. A copy of a twofold value, a slice, a map or a pointer or what holds one, still names what the caller's does (the items, the map, the value pointed to), so what the task writes there the caller sees, and it can't be given what a fixed name owns (see What a fixed name owns); through a plain parameter nothing is written, at any depth.

task countdown(flex n: int, step: int) -> int
    flex steps = 0
    loop n > 0
        n = n - step
        steps = steps + 1
    fi
    <- steps
fi

task main()
    fixed a = 10
    show(countdown(a, 3))    ; 4
    show(a)                  ; 10: the caller's value never changes
fi

Since the caller's value never changes, the call needs no mark, and any value can be given, a fixed or a literal too. A slice is a copy of the caller's slice: adding to it leaves the caller's length as it was, and the items the two still share are the same items, as after ys = xs (see Adding and taking out items). A method takes one too: alters grow(flex by: int). A region never changes, so flex r: region is an error, and only a parameter is flex: a shape's field changes where the value it is part of is held.

Written flex&, a parameter is the caller's place itself: every change the task makes to it is a change there. The call marks the place it gives with &, so whoever reads the call sees what may change:

task swap(flex& a: int, flex& b: int)
    a, b = b, a
fi

task main()
    flex x = 1
    flex y = 2
    swap(&x, &y)          ; x is 2, y is 1
    flex pair = [1, 2]
    swap(&pair[0], &pair[1])
fi

A place is given only where it can change: a flex variable, a field or an array item of one (&c.n, &pair[0]), what a pointer reached from a flex name points to (&p^, &p^.n), or a flex& or flex parameter, passed on. The rest is refused, each with the reason:

flex& is one word: flex &x is an error that says so. In C, such a parameter is a pointer to the place, as the value an alters method is called on already is.

Parameters that bind

A bind x: T parameter is like flex x: T, but it can never be assigned as a whole: x = … is an error. Its contents may still change: fields and items assigned, what a pointer points to, and a slice's items or a map's entries changed. For a simple type such as int, which has no contents apart from the value, bind acts exactly like fixed. A bind parameter is given by value, and like flex it can't be given what a fixed name owns if the value is twofold (see What a fixed name owns).

task clear(bind xs: [int]@r)
    xs[0] = 0
    xs[1] = 0
fi

task main()
    region r
    flex xs = [1, 2, 3, ...] at r
    clear(xs)
    show(xs)                 ; [0, 0, 3]
fi

Written bind&, a parameter is the caller's place, only its contents may change and it is never assigned as a whole. The call marks the place with &, as for flex&. A bind& place must be one whose contents can change, so a fixed variable of a twofold type can't be given to it; a fixed value with no contents, a number or a tuple, say, can, since on such types bind& and fixed& act the same.

task set_first(bind& xs: [int]@r, v: int)
    xs[0] = v
fi

task main()
    region r
    flex xs = [1, 2, ...] at r
    set_first(&xs, 9)
    show(xs)                 ; [9, 2]
fi

A bind or bind& parameter can't be assigned as a whole, can't have its length changed with add or del, and can't be given to a flex& place, which may be assigned as a whole. A shape's field is never bind: it changes where the value it is part of is held (a shared field may be shared bind). In C, bind is a plain parameter and bind& is a pointer to a place that changes, or, of a type with no contents, to one only read, as fixed& is.

Parameters that read

A plain parameter, x: T, leaves the compiler to choose how it is given: a copy, or, for a large value, where the caller keeps it (see large values). Nothing the task does tells the two apart, except == on a shape, an array, or a tuple, and &x, which read where the value is, and which the compiler warns about. Two words say which one is meant:

shape Point
    x: int
    y: int
fi

task one(fixed& a: Point, fixed& b: Point) -> bool
    <- a == b                ; where they are: one place or two
fi

task sum(fixed& xs: [int#1000], fixed start: int) -> int
    flex total = start
    each x in xs
        total = total + x
    fi
    <- total
fi

task main()
    fixed p = Point { x: 1, y: 2 }
    fixed q = p
    show(one(&p, &p), one(&p, &q))   ; truefalse
    show(one(p, q))                   ; error: task 'one' reads 'a' where the caller keeps it, so the call marks that place with '&'
fi

A fixed& parameter can't be assigned, have its fields assigned, be added to, be given to a flex&, or call an alters method, each an error that says it only reads. It is read while the task runs, so no place given beside it in the same call may change: f(&a, &a) where f takes fixed& x and flex& y is refused, and so is c.grow(&c) where grow alters c, though a plain copy of c would be read before the method runs. It names no item of a slice, which moves when the slice grows, and no value made on the spot (&f(), &t[0..2]); the place is of exactly the parameter's type. A region is given as it is, so fixed r: region and fixed& r: region are errors, and only a parameter is fixed. In C, fixed& is a pointer to a const place, and fixed is a plain parameter.

Warnings

A warning is something a program does that it may not mean; unlike a mistake, it doesn't stop the program from building. The vela command writes each to standard error before it goes on, as file:line:col: warning: … with the line and a caret, the way it writes a mistake; while a program has a mistake, only the mistakes are shown. Vela Studio marks warnings in place and lists them under Problems, apart from errors.

One warning is about a plain parameter, x: T, whose giving the compiler chose (see large values): == on a shape, an array, or a tuple, and &x, read where the value is, so they depend on that choice. The warning says which way the compiler went, from the type's size, and how to say which is meant:

task same(a: Point, b: Point) -> bool
    <- a == b     ; warning: '==' compares Point by where it is, and 'a' is a parameter, given as the compiler chose:
                 ; Point takes 16 bytes, so 'a' is a copy, a place of its own, never the caller's;
                 ; write 'fixed& a: Point' for the caller's place, 'fixed a: Point' for a copy of its own, or compare items with '~='
fi

A part of such a parameter warns too (line.from == p), and so does the value a method is called on, .this, which is given the same way, and a kind whose variants carry such values. Two parts of one parameter (line.from == line.to) warn about nothing, since they are where they are alike whichever way it was given, and neither does an item of a slice or what a pointer points to, p^, which are places of their own however the slice or the pointer was given. Writing fixed or fixed& says which is meant and silences it; in place of ==, so does ~=, which compares items wherever they are. In a task that takes types, the warning comes with each use that gives such a type, naming it: the body was checked when any type could be given, and a number compares by value. Vela Studio shows the open file's warnings; a module's own are shown where it is open.

Two more are about loops that never end. A loop written to never end by itself, loop || ||, loop ||, loop true, or loop || true, is told to say so with endless (see A loop in three parts). And a loop whose condition reads only variables that nothing in the loop changes, with nothing in it that leaves, never ends once it starts:

task wait(x: int)
    loop x < 10              ; warning: this loop never ends: its condition reads only 'x', which nothing in the loop changes, and nothing in it leaves the loop
        touch(x)               ; a copy of x, which can't change it
    fi
fi

It looks only at simple cases, and costs one walk over the loop: a condition of variables, values, fields and operators, each variable a plain value of the task, and a loop with no break or <- in it. Anything that may change a variable counts as changing it: setting it or a part of it, a result set with total <- …, giving it with &, calling a method on it or changing a slice in it (b.items.add(1)), or a lambda keeping it as a place; and a condition that calls a task, or reads through a pointer, a reference or a shared field, is never told, since those may change elsewhere.

Others are about slices that share items (see Copies and parts), where what is done to one may or may not show in the other, since an add keeps the two on the same items only when it finds room in place, and a del moves items in the memory they share:

Each of these is said once in each task, for the first place it happens (one warning for the first add on a parameter, one for the first del, and so on), and the char-array warning once in each file (see Chars); the build's --repeat-warnings shows every place, each in its line. The compiler follows only copies made by a binding or an assignment of a slice's own name or a part of it, within one task, in the order they are written: not slices in fields, given to a call, or kept by a lambda, and not what a loop's next pass does before an earlier line.

References

A block can name a place too. flex& y = &x makes y another name for x, so a change to either is a change to both, and fixed& y = &x names it only to read it, as it is whenever it is read:

flex x = 1
flex& y = &x          ; y is another name for x
y = 5                    ; x is 5
x = 7                    ; y reads 7

flex c = Counter {}
flex& count = &c.n    ; a field, an array's item, or p^ too
count = count + 1

fixed a = 1
fixed& b = &a          ; b reads a, and never changes it
flex& d = &a          ; error: 'flex& d' names a place to change it, … and 'a' never does …;
                        ;   'fixed& d = &a' names it only to read it

flex& takes the places a flex& parameter does, and fixed& takes those and also a fixed, a parameter, and another fixed&. bind& y = &x names a place to change its contents, y.value = 7, never assigning it as a whole: it takes a flex or bind place, and of a type with no contents it only reads, as fixed& does. Neither names an item of a slice, which moves when the slice grows, a result, or a value made on the spot. A reference is kept safe by what it can't do, so it needs no lifetimes:

What a reference can't see is a pointer: two pointers that hold one address, or a pointer to the place a fixed& reads, reach it in a way the compiler doesn't follow. Those concerns, and where each stands, are listed in Safety.

In C, a reference is a pointer to the place, which an optimizing C compiler usually removes, using the place itself.

Addresses

&x also says where a place is, in four spots only: show(&x) writes its address, (&x).str() at r, text(&x) at r, and "{&x}" at r make it text, all in C's hex, 0x7ffd…, which int(text) reads back, and &a == &b (or !=) asks whether two places are one, whatever they hold. A place is what a reference names: a variable, a field or an item of one, what a pointer points to, a shared field, or .this; a reference is the place it names. A length, xs.len, is worked out from the value, not kept in it, so it has no place.

flex p = Point { x: 1, y: 2 }
fixed q = p                   ; a copy, in a place of its own
flex& also = &p
show(&p)                      ; 0x7ffd5e8c1a40
show(&p == &q, &p == &also)   ; falsetrue
fixed ptr = new Point(p) at r
show(&ptr^ == ptr, &p == ptr) ; truefalse: a pointer points at its own place
show(&p == &p.x)              ; error: places of two types are never one place

Both sides are of one type: a shape and its first field start at the same address and are still two places, so &p == &p.x is an error. One side may be a pointer ^T to the other's type, &x == p, which asks whether p points at x; a maybe is opened first. An address is no value of its own: fixed a = &x, (&x).type, &a < &b, and &x in a tuple are errors, and so is & on what has no place of its own: a call, a literal, a variant, or a part taken of text (&t[0..2]). A parameter written x: T is a copy or, when large, the caller's place (see large values), so its address says which the compiler chose.

Several Results

A task can send more than one value: its header lists the types after ->, and <- sends one value for each. A list of names receives them.

task divmod(a: int, b: int) -> int, int
    <- a / b, a % b
fi

task main()
    fixed q, r = divmod(17, 5)
    show(q)                      ; 3
    show(r)                      ; 2

    fixed whole, _ = divmod(9, 4)  ; _ drops a result
    show(whole)                  ; 2
fi

The results are separate values, not one value, and not a tuple: show(divmod(17, 5)) and fixed pair = divmod(17, 5) are errors that say 'divmod' sends 2 values; receive them with 'fixed a, b = …'. A task that sends one tuple says so in its header, -> (int, int) (see Tuples). A task with several results can only be called as the whole right side of a list, which includes a <- that sends them on:

task halves(n: int) -> int, int
    <- divmod(n, 2)         ; sends both of divmod's values
fi

Each result follows the rules of a single one: -> maybe int, bool and -> ^Node@r, int work as they read, and the regions of a call's results are settled at the call (see Memory and Regions). One value can't be nothing, since nothing means no value at all. A value sent on from a call, as in halves above, goes as it is: it doesn't become a maybe or a T ~ E on the way, which a single sent value would, and the error says so. Results fail one at a time, not together: -> int, int ~ Bad is two results, the second of which may fail, and its receiver gives that one a plan B. A task that fails as a whole sends a single T ~ E.

fixed, flex, and assignment take the same lists with plain values too, and every value is computed before the first of them is stored, so two variables can be swapped in one line:

flex a = 1
flex b = 2
a, b = b, a                  ; a is 2, b is 1
fixed x: int, y: float = 3, 0.5

A name can appear once on the left of an assignment, and the names of a fixed or flex must all be new, as any binding's must. A _ on the left takes a value and drops it, except a T ~ E, whose error must be handled.

Named Results

A result can have a name, written like a parameter. The name is a variable of the task, and the same arrow that sends results sets it. What stands before the arrow says what happens:

Header Line Meaning
-> int <- value Send the value and end the task
-> int, int <- a, b Send both values and end the task
-> total: int total <- value Set the result and keep going
-> total: int <- value Send that value and end the task
-> total: int <- Send the results as they are and end the task
-> total: int the closing fi Reaching the end sends the results

So an empty left side means "send and stop", and a name on the left means "this is the result so far":

task score(hits: int, misses: int) -> total: int
    total <- hits * 10
    when misses == 0
        <- total + 50            ; a perfect round: send the bonus score and end here
    fi
    total <- total - misses * 5  ; otherwise keep going
    when total < 0
        total <- 0
    fi
fi                               ; reaching the end sends total

Setting a result never ends the task. With several names, one line can set them all, and a bare <- sends what they hold:

task collatz(start: int) -> steps: int, peak: int
    flex n = start
    steps, peak <- 0, start
    loop n != 1
        when n % 2 == 0
            n = n / 2
        else
            n = 3 * n + 1
        fi
        steps <- steps + 1
        when n > peak
            peak <- n
        fi
        when steps == 1000
            <-                  ; give up and send what we have
        fi
    fi
fi

The rules:

Bindings

fixed creates an immutable binding. flex creates a mutable variable. bind creates a name bound to one value: it can never be assigned as a whole, but its contents may change. hold creates a variable that can be reassigned as a whole, but its contents cannot be changed. Written fixed&, flex&, bind&, or hold&, each is another name for a place instead (see References).

fixed pi = 3.14
flex count = 0
count = count + 1
flex total: float = 0.0     ; the type can be written out
bind scores = [0, 0, ...] at r
scores[0] = 10              ; the slice's items may change
; scores = other           ; error: a 'bind' name stays bound to one value
hold origin = Point { x: 0, y: 0 }
origin = Point { x: 10, y: 20 }  ; ok: reassigned by whole
; origin.x = 5              ; error: cannot assign to a field of 'hold' variable

What a bind name allows. Its contents change; it is never assigned as a whole:

bind is written where flex is: before a parameter (see Parameters that bind), a reference, a case form's names, a loop's start, a shared field, in a lambda's list, and in a task's type, each below.

Steps. x++ and x-- count a place up or down by one, and x += v gives it x + v, as do -=, *=, /=, %=, and **= with their operators. Each is a statement on a line of its own, as = is, and gives no value: show(x++), ++x, and y = x += 1 are errors. A step is the assignment it stands for, with that operator's types and stops: the place must be one that can change (a flex variable, or a field, an item, a map's value, or what a pointer points to), an int that overflows stops the program, x /= 0 is refused, and x **= k on an int asks for a power that stays an int.

flex count = 0
count++                     ; 1
count += 10                 ; 11
count **= 2                 ; 121
flex c = 'a'
c++                         ; 'b': a char counts to the next one, and a unit counts too
flex f = 1.5
f++                         ; error: '++' counts a whole number or a char by one, and this is a float; for a float, write 'x += 1.0'
words[w] += 1               ; stops when w is no key of the map, as words[w] does
flex t: text@r = "a" + "b" at r
t += "{count}!"             ; "ab121!", joined into r, the region t is in

On text, += joins, into the region the text is in, so text that lasts as long as the program (flex t = "ab") can't take it: write t = t + u at r, or keep the text in a region. The place is named twice, read and then set, so nothing in it may call a task: xs[next()] += 1 asks for the index in a variable first.

Assigning copies the value. Shapes are copied whole, so after flex b = a, changing b.x leaves a alone.

Passing a value to a task copies it as well, as far as the program can tell. A task can't change a parameter not written flex, so for a large one (a type of more than 64 bytes, such as a shape of nine ints or an array of them) the compiler passes the address of where the caller keeps it instead, which saves copying it at every call. It does so only where nothing can change the value while the task runs: a local variable, or a field or array item of one, given where no later argument calls a task. A value behind a pointer, in a slice or a shared field, or named by a reference is copied first, since the task may reach that memory another way. Nothing a program does shows the difference, except that it runs faster, and that &x on such a parameter, and == on a shape, an array, or a tuple, read the caller's place rather than a copy's (see Addresses). A parameter written fixed x is always a copy, and fixed& x always the caller's place.

A variable lives until the block that declares it closes. Vela has no shadowing: inside a task, a name cannot be declared again while another variable with that name is visible (including parameters). Separate blocks, such as the two branches of a when, may reuse a name.

when ready
    fixed label = "go"
    show(label)
else
    fixed label = "wait"   ; fine: a different block
    show(label)
fi
show(label)                ; error: unknown variable 'label'

Whole values, twofold values, and places

What a copy of a value does sorts every value into one of two kinds, and a place is where one is kept:

flex a = Point { x: 1, y: 2 }  ; whole
flex b = a
b.x = 5                         ; a.x is still 1
flex xs = [1, 2, ...] at r    ; twofold
flex ys = xs
ys[0] = 9                       ; xs is [9, 2] too: one set of items
flex zs = xs.copy() at r        ; items of its own, at every depth

The rules about change follow from the kind. What a task does to a whole value it was given is its own, so flex, bind and fixed matter to others only for twofold values and places: what a fixed name owns, a twofold value, is never given to what may change it (below); copy() and new [T](xs) copy a twofold value's items too (see Copying into another region); a task's type says flex for a twofold value the task changes, and flex& or fixed& for a place, but nothing for a whole value (see A value the task changes through); and the call marks a place it gives with &.

What a fixed name owns

Nothing changes through a fixed name, at any depth: not a field, not an item, not what a pointer it holds points to. fixed p = new Point(…) at r then p^.x = 5 is an error, and so are xs[0] = 9 and xs[0].bump() on a fixed slice. A slice, a map and a pointer are twofold, so a copy of one names the same items, and a flex name given them would change what the fixed one holds. So what a fixed name owns is never given to what may change it:

Each is an error where the value is given, which names the owner and the copy to write instead: 'ys' is given what fixed 'xs' owns, and nothing changes through what a fixed name owns; give it a copy: 'xs.copy() at r'; a new slice with one change, xs.added(v) at r, owns nothing either. A fixed name owns what is reached from it (its fields, its items, what its pointers point to), a value made with such a part (a shape, tuple, array or variant holding it, or a plan B or a match that may give it), and what a call gives back of it. A task may send back what a parameter holds, as first_two(xs) sends xs[0..2], and then the call's value is what xs owns too; a task that only reads what it is given to make something new, as one counting the lengths of a list of names does, gives back nothing a fixed name owns. What fail sends is given back too, as the failure. m.keys at r and m.values at r are slices of their own, which share only what each key or value holds. A lambda that keeps what it uses holds what its call may send back: [@r]() -> [int]@r <- xs fi may only be kept in a fixed name or given where nothing changes it, and what its call gives back is what xs owns, but one that only reads xs to make a new slice gives back nothing owned. A whole value, which holds no slice, map or pointer, is copied whole, so a fixed number, text, or shape of them may be given anywhere. Parameters not written flex, fixed& references, case form and loop names without flex, and .this outside alters are fixed names too. In a task or shape that takes types, the rule is checked for each set of types it is given, and the message names the set that breaks it: flex y = x with x: T is fine where T is int, and an error where 'keep' is given [int].

A list a fixed name owns is walked by a task that calls itself for the rest of it, since no flex cursor may be given it:

task sum(list: maybe ^Node@r) -> int
    when list is {node}
        <- node^.value + sum(node^.next)
    fi
    <- 0
fi

A list the program changes is kept in flex names, and walked with a flex cursor and a flex name for each node: loop current is {flex node}, then current = node^.next. What a flex name holds is still seen through a fixed one made of it: after fixed zs = ys, a change through ys shows through zs, which only promises that nothing changes through zs itself.

Numbers

Arithmetic (+ - * / %, and **) works on int, float, unit, and the sized numbers, on two numbers of the same type: two ints give an int, with division truncating toward zero, and two floats give a float. An int and a float don't mix, so n + 2.5 is an error that asks for float(n) + 2.5 (or n + int(2.5), which truncates); no int becomes a float quietly, as a binding (fixed x: float = 3), an argument, and a field already ask for one. % on floats keeps the sign of the dividend. On ints too, % keeps the sign of the dividend (-7 % 3 is -1). Comparisons (< <= > >=) work on numbers of the same type. Number literals are decimal digits with an optional fraction (42, 3.14), and _ may separate digits (1_000_000); there is no exponent form, so 1e5 is an invalid number. An int literal can be at most 9223372036854775807, so the smallest int is written -9223372036854775807 - 1.

A whole number may be written in another base after 0 and a lowercase letter: 0x hex (0x1F, digits 0-9 and a-f in either case), 0o octal (0o755), and 0b binary (0b1010_0101), with _ between digits as in a decimal one. It is an int like any other, at most 0x7fff_ffff_ffff_ffff, and always whole. A leading 0 alone is only a digit, so 010 is ten, not C's octal eight. A digit the base doesn't have ('2' is not a binary digit (0 or 1)), an uppercase base letter (0X1F), no digits after the base, and an underscore that isn't between two digits are each an invalid number that says which. int(text) reads the same bases.

show(0x1F, " ", 0o17, " ", 0b101)      ; 31 15 5
show(0xff_ff + 1)                  ; 65536
show(0b102)                        ; error: invalid number '0b102': '2' is not a binary digit (0 or 1)
show(7 / 2)              ; 3, two ints
show(float(7) / 2.0)     ; 3.5, two floats
show(5.5 % 2.0)          ; 1.5
show(7 / 2.0)            ; error: an int and a float don't mix in arithmetic …

An int never wraps around. An operation whose result doesn't fit in an int, and a division or remainder by zero, stop the program with a message saying where, on every C compiler:

task average(total: int, count: int) -> int
    <- total / count
fi

task main()
    show(average(10, 4))     ; 2
    show(average(10, 0))     ; stops: vela: division by zero at 2:14 in task 'average'
fi

The one quotient that doesn't fit is the smallest int divided by -1, and -x overflows only when x is the smallest int. A remainder always fits: the smallest int % -1 is 0. Arithmetic on floats never stops the program: 1.0 / 0.0 is infinity. Arithmetic written with literals only (60 * 60 * 24) gives the same result every time, so the compiler computes it and rejects one whose result doesn't fit (integer overflow: the result, 9223372036854775808, is more than an int holds (at most 9223372036854775807)), and a division or remainder by a zero written that way, as in n / 0 (division by zero).

Powers. a ** b is a raised to the power b. It comes before * and / (2 * 3 ** 2 is 18) and groups from the right (2 ** 3 ** 2 is 2 ** 9, 512). A - before a value takes that value alone, so -2 ** 2 is (-2) ** 2, 4, where Python and written math give -4; write -(2 ** 2) for that. The type depends on what can be known when compiling:

show(2 ** 10, " ", 2 ** -1, " ", 2.0 ** 0.5)    ; 1024 0.5 1.41421
flex k = 3
show((2 ** k).type)                ; float
show(2 ** unit(k))                  ; 8, an int
flex x: int = 2 ** k             ; error: expected type int, got float: an int raised to an int that may be negative is a float …

Units. A unit is a whole number from 0 to 255 held in one byte, as Rust's u8 and C's uint8_t are. There is no unit literal: a number written out becomes a unit where one is expected (flex b: unit = 200, an argument, a field, an item, a maybe unit), and beside one (b + 1, b < 16, m ~= 7), and one that doesn't fit is an error (256 doesn't fit in a unit, which holds 0 to 255). Arithmetic on units gives a unit, and like an int it never wraps around: a result below 0 or above 255 stops the program (vela: 256 does not fit in a unit at 5:15 in task 'main'). A unit is never negative, so -b is an error. An int and a unit don't mix, as an int and a float don't: int(b) + n or b + unit(n) says which. Units compare, order, show, and turn into text as numbers do, and a generic's ordered takes them.

flex b: unit = 250
b = b + 5                   ; 255
show(b, " ", b.type)          ; 255 unit
show(int(b) + 1)             ; 256, an int
fixed n = 3
show(b + n)                 ; error: an int and a unit don't mix in arithmetic …
b = b + 1                   ; stops: vela: 256 does not fit in a unit …

Number sizes. Besides int, float and unit, a number may have a size of its own: the signed int8, int16 and int32, the unsigned uint16, uint32 and uint64, and float32. Each is C's fixed-width type (int8_t … uint64_t, float). int64, uint8 and float64 are other names for int, unit and float, and uint is uint64. They follow the rules units do:

flex small: int8 = 100
small += 27                 ; 127
fixed big: uint64 = 9_223_372_036_854_775_807
show(big + big + 1)          ; 18446744073709551615
show(int32(small) ** 2)       ; 16129
small++                     ; stops: vela: 128 does not fit in an int8 …

Chars. A char is one byte, as C's char is: the code of a character, from 0 to 255, so it holds any byte of text. It is written between single quotes, 'a', with the escapes text has ('\n', '\t', '\0', '\\') and '\'' for the quote. Only one byte fits: 'ab' and 'é' (two bytes in UTF-8) are errors that say to write text instead. A number moves a char, c + 1, 1 + c, c - 1, and gives a char; the distance from one char to another, 'z' - 'a', is an int; a result below 0 or above 255 stops the program (vela: 260 does not fit in a char), and one written out is an error. No other arithmetic takes a char: 'a' + 'b', c * 2, and -c are errors, and int(c) * 2 works on its code. char(n) turns a code into a char, stopping the program outside 0 to 255, and int(c) and unit(c) give the code back. Chars compare and order by their codes, so c >= 'a' and c <= 'z' tells a small letter. show(c) writes the character itself; inside another value it is written as code writes it, ['a', '\n'], and a byte no escape writes as char(200).

Text already holds characters, with its length, parts, t[i], joining, and ==, so an array or slice of chars is text built by hand: the compiler warns about the first one made in each file (this builds text by hand as chars; 'text' holds characters already …) and says nothing about the rest, unless the build is given --repeat-warnings.

task shout(c: char) -> char
    when c >= 'a' and c <= 'z'
        <- c - ('a' - 'A')       ; a char moved by an int
    fi
    <- c
fi

task main()
    fixed word = "vela"
    flex i = 0
    loop word.at(i) is {c}          ; none past the end
        show(shout(c))                  ; V, E, L, A
        i = i + 1
    fi
    show(word[0], " ", int('A'), " ", char(66))   ; v 65 B
    show('a' + 'b')                   ; error: two chars don't add up to a char …
fi

float(n), int(x), and unit(x) turn a value into a number, called by the name of the type they give, as text(x) at r is. They make no memory, so they name no region. float(n) turns an int or a unit into a float exactly, and never fails; so does int(b) on a unit. unit(x) takes what int(x) takes, and stops the program when the whole number isn't from 0 to 255 (300 does not fit in a unit); written out, unit(300) is an error. char(n) takes an int, a unit, or a char, the same way (see Chars); a byte of text is t[i], not char(t). int(x) takes a float or a bool: a float becomes its whole part, toward zero, so it rounds neither way (int(2.9) is 2 and int(-2.9) is -2), and a bool becomes 1 or 0, which counts (int(n > 3)). A float with no whole part to give stops the program, as arithmetic that doesn't fit does: int(0.0 / 0.0) says nan cannot become an int, and a float too large to hold says inf does not fit in an int. A number turned into its own type, int(n), is the value itself. A bool reaches float through int, float(int(ok)), and rounding another way waits for conversions of its own.

Reading a number from text. int(t), float(t), and unit(t) on text may fail, since the text comes from outside the program, so they send int ~ ParseError, float ~ ParseError, and unit ~ ParseError, read with a plan B, match, or is like any error result. ParseError is a kind no program declares, with two variants and no values in them: NotANumber, when the text is no number at all, and TooLarge, when it is a number the type can't hold ("9223372036854775808" as an int, "300" or "-1" as a unit). A program may send one itself, written through its kind (fail ParseError.TooLarge) or as a bare variant (fail TooLarge), and no declaration may take the three names.

The text is the number and nothing else: digits, an optional leading -, and for a float one . with digits on both sides. An int may be written in another base after its sign, as code writes one: int("0x1F") is 31, int("-0b11") is -3, and int("0o17") is 15, with the base letter lowercase and hex digits in either case; scan(prompt, int) reads them too. A space (" 4"), a +, a _ (which a written number may hold), an exponent ("1e5"), a fraction read as an int ("4.0"), and text with nothing in it are all NotANumber. Leading zeros are fine ("007" is 7, and "010" is 10). A float is read to the nearest one it can hold, from at most 400 significant digits, and one too large to hold is TooLarge; one too small is 0, as arithmetic gives.

A number read straight from input is scan(prompt, int) (or float, or unit), which sends an InputError instead, since reading input can also end.

task main()
    region r
    show(int("42") { 0 })               ; 42
    show(int("4x2") { -1 })             ; -1, the plan B's value
    fixed line = scan("Age: ") at r { "" }
    match int(line)
    case {age}
        show("next year: ", age + 1)
    case failure why
        match why
        case NotANumber
            show("that is no number")
        case TooLarge
            show("that number is too large")
        fi
    fi
fi
task main()
    fixed n = 7
    show(float(n) / 2.0)                 ; 3.5
    show(int(2.9), " ", int(-2.9))       ; 2 -2
    show("over three: ", int(n > 3))    ; over three: 1
    show(int(1.0 / 0.0))               ; stops: vela: inf does not fit in an int at 6:10 in task 'main'
fi

Operators bind from loosest to tightest as follows; operators on the same row group from the left.

Operators Example
or a or b and c is a or (b and c)
and
== != ~= <>
< <= > >= 1 < 2 < 3 is an error: 1 < 2 is a bool
|> |>> … a + 1 |> f() == 3 is f(a + 1) == 3 (see Call pipes)
.. 0..n + 1 is 0..(n + 1)
+ -
* / % 2 + 3 * 4 is 14
-x, not x, fail e, &x not a == b is (not a) == b; write not (a == b). &p^.x is the place p^.x
a.x, p^, xs[i], f(x), x { … } p^.x reads a field through a pointer; x { 0 } * 2 is (x { 0 }) * 2

at r after text joined with +, or after a call, names the region of the whole chain before it: first + " " + last at r (see joining text and the region after a call).

Text, Output, Input, and Logic

Text literals use double quotes and support the escapes \n, \t, \r, \\, \", \0, and \{ and \} for braces, since a brace in text holds a value (see Text holding values). A ; starts a comment that runs to the end of the line (and a value in text's braces can't hold one). A text literal ends on the line it starts (a line break in it is written \n). Text holds any UTF-8 and knows how long it is: t.len is how many bytes it has, read without counting them. A character outside ASCII takes more than one byte ("é".len is 2, "Ada".len is 3), and a \0 is a byte like any other ("ab\0cd".len is 5, and it shows all five). Floats print with up to 6 significant digits, switching to exponent form for very large or small values (123456789.0 shows as 1.23457e+08), and 0.0 / 0.0 and 1.0 / 0.0 show as nan and inf.

show takes any number of values and writes them one after another, with nothing between them, then ends the line: show("n = ", n) prints n = 5, and any space between the values is one you write. A number, a bool, and text are written as they are. An array or a slice is written [1, 2, 3] (an empty slice []), a tuple (1, "a"), and a maybe none or { 4 }, so it reads apart from the value itself; to write only the value, open it first, as with a plan B, show(m { "none" }). Text inside any of them is written in quotes, with its quotes, backslashes, braces, line breaks, and tabs escaped as a literal writes them, so ["a, b"] and ["a", "b"] read apart, and ["\{x\}"] is text rather than a value. A pointer is written as the address it holds, in C's hex, 0x7ffd…, which int(text) reads back; the value it points to is p^, written as its own, and where any place is, show(&x) (see Addresses). A shape is written as a literal of the fields any file reads, Point { x: 1, y: 2 } (its priv fields and its shared ones are no part of it, and one without any is Marker {}), by its own name wherever it is shown, and Box<int> { value: 3 } for one that takes types; a kind is written as the variant it holds, with the values it carries as a call gives them, Circle(1.5) or Dot. A shape or kind with a method str of its own is written by it instead, on its own or inside any of these: show(card) writes what card.str() gives. The text that takes goes in a region show makes for the line and frees once the line ends, so printing a value of your own names no region at all. Everything else is written straight to the output, with no memory made for it. A value that can fail (T ~ E) is read first, so it, and anything holding one, is an error that says so. show() writes an empty line. The values are written, and anything they ask for happens, left to right, so show("a:", f(), " b:", g()) calls f before g however the C compiler orders a call's arguments (see the limits). A sentence with values in the middle of it reads better as text holding values, which keeps the text to use again; show is for printing it once.

shape Card
    name: text
    wins: int
    str(r: region) -> text@r
        <- "{.name} ({.wins})" at r
    fi
fi

task main()
    fixed n = 5
    show("n = ", n)                      ; n = 5
    show(n, " and ", n * 2, "!")        ; 5 and 10!
    show()                                ; an empty line
    fixed missing: maybe int = none
    show("missing: ", missing)            ; missing: none
    show([n, n * 2], " ", (n, "five"))           ; [5, 10] (5, "five")
    fixed found: maybe text = "Ada"
    show(found, " ", found { "nobody" })        ; { "Ada" } Ada
    fixed card = Card { name: "Ada", wins: 3 }
    show("top: ", card)                   ; top: Ada (3), from Card's str
fi

t[a..b] is a part of text: its bytes from a up to, but not including, b. A part is the same bytes, not a copy, so it lives where the text does: a part of text is text, and a part of text@r is text@r. Its bounds are checked as a slice's are, and each must fall between two characters: "café"[0..4] would end inside the two bytes of é, so it stops the program (vela: text 0..4 cuts through a character at byte 4). t[i] is the byte at i, as a char; an index out of range stops the program, as an array's does. t.at(i) reads the same byte as a maybe char, which is none out of range, so a loop can walk text without its length (loop t.at(i) is {c}). A byte is the byte, not a whole character: "é"[0] is the first of the two bytes of é. Text never changes, so neither a part nor a byte can be assigned, and a reference can't name a byte.

<, >, <=, and >= order text by its bytes, which is the order of Unicode: capitals come before small letters ("B" < "a"), and text that starts other text comes before it ("app" < "apple"). Text of any regions compares.

task first_word(line: text@r) -> text@r
    flex i = 0
    loop i < line.len
        when line[i] == ' '
            <- line[0..i]              ; a part: text@r, like the line
        fi
        i = i + 1
    fi
    <- line
fi

task main()
    fixed name = "Ada Lovelace"
    show(name.len)                        ; 12
    show(name[4..name.len])               ; Lovelace
    show(first_word(name))                ; Ada
    show("café".len)                      ; 5: é takes two bytes
    show("café"[3..5])                   ; é
    show("apple" < "banana")            ; true
fi

scan(prompt) at r, or scan(prompt)@r, reads input: it shows the prompt (as it is, with no newline added; "" shows nothing), and everything shown before it, then reads one line into region r, without its line break (a Windows \r\n too). The line is new text, so it goes in a region, as new's values do (see Text in a region), and the task needs that region itself. It gives a maybe text@r: none once the input has ended (Ctrl-D, or the end of a file given as input), so reading every line is a loop … is {line}. A last line without a line break is still a line, a line can be as long as memory allows, and a \0 in it is one of its bytes. As a statement, scan("Press Enter")@r reads a line and drops it.

task main()
    region r
    fixed name = scan("Your name: ") at r { "stranger" }   ; text@r; "stranger" when input has ended
    show(name)
    flex count = 0
    loop scan("")@r is {line}                              ; every line left
        count = count + 1
    fi
    show(count)
fi

scan(prompt, int), scan(prompt, float), and scan(prompt, unit) read a number from that line instead of the line itself. A number needs no memory, so this form names no region, and it gives int ~ InputError: the number, or why there is none. InputError is a kind no file declares, with two variants — Ended, the input is over, and NoNumber, which holds the ParseError that reading the line gave (see Reading a number from text), read with a match of its own. A program reads one with a plan B, match, or is, passes on one it was given, and may send one itself (fail Ended); no declaration may take those names.

task main()
    flex total = 0
    flex reading = true
    loop reading
        match scan("n: ", int)                ; int ~ InputError; no region
        case {value}
            total = total + value
        case failure e
            match e
            case Ended                        ; nothing more to read
                reading = false
            case NoNumber why                 ; the line was no number
                match why
                case NotANumber
                    show("that is no number")
                case TooLarge
                    show("that does not fit in an int")
                fi
            fi
        fi
    fi
    show("total ", total)
    fixed age = scan("Age: ", int) { 0 }        ; a plan B where any trouble means the same
    show(age)
fi

Which form to use: scan(prompt, int) where a number is all the program wants, and scan(prompt) at r with int(line) where it also wants the line — to print what it refused, to accept "quit" beside numbers, or to read two numbers from one line. A loop that reads for a long time can keep each line in a region declared inside it, which is freed on every pass; a loop that reads numbers keeps nothing.

Logic uses words: and, or, and not work on bool, and and/or skip the right side when the left side decides the result. Conditions in when and loop must be bool: when count is an error. == and != compare each type by a rule of its own, and ~= compares two values item by item (see Comparing). <, >, <=, and >= work on numbers, chars, and text.

Comparing

== is cheap for every type, so what it compares depends on the type: a number and a bool by value, text by its bytes, a pointer by the address it holds, a slice by its view (the same first item and as many items, so two views of the same items are equal and a copy is not), and a kind by its variant, with what the variant carries compared by that value's own rule. An array, a shape, and a tuple are compared by where they are: a == b is true when both name one place, as a reference and its place do, and a copy is another place. So each side of such a comparison is a place (a variable, a field or an item of one, or what a pointer points to), and a value made on the spot, such as a literal or a call's value, is an error that suggests ~=. A kind that can carry an array, a shape, or a tuple is compared the same way; one carrying only numbers, bools, text, pointers, or slices compares freely, mood == Mood.Calm. Both sides have the same type, though values in different regions compare (pointers and places in two regions are never equal). A maybe compares with == only with none, so an empty value is never mistaken for one that holds something; ~= compares what it holds (below, and see comparing a maybe on purpose). A value that can fail is read first.

a ~= b asks whether two values are similar, item by item, which costs as much as the values are long, hence a symbol of its own; a <> b is not (a ~= b). Numbers, bools, and text compare as == has them; arrays, slices, and tuples item by item; a shape by every field but its shared ones, priv ones too, as they are part of the value; a kind by its variant and what that carries; and a maybe when both are empty or both hold similar values (m ~= none is m == none). A maybe is also similar to a plain value of the type it holds, either side first, when it holds a value similar to it: m ~= 10 is false when m is empty, and m <> 10 is its not. A tuple or array written on one side becomes the other side's type where it can, as it does a written type, so t ~= (1, none) compares a (int, maybe int). A pointer is similar to another holding the same address, so nothing loops through a list that points back to itself; p^ ~= q^ compares what they point to. A value that can fail is read first.

flex p = Point { x: 1, y: 2 }
fixed q = p
flex& also = &p
show(p == q, " ", p == also, " ", p ~= q)        ; false true true
show(p == Point { x: 1, y: 2 })                 ; error: '==' compares Point by where it is; '~=' compares item by item
show((1, "a") ~= (1, "a"), " ", [1, 2] <> [1, 3]) ; true true
fixed s = new [int]([1, 2]) at r
fixed view = s
show(s == view, " ", s == s.copy() at r, " ", s ~= s.copy() at r)  ; true false true

A small parameter is a copy (see large values), a place of its own, so == on two shapes given to a task is false even when they are one value outside it; compare them with ~= there, or take them as fixed&, the caller's places; the compiler warns where a plain parameter is compared so. Where two places themselves are meant, &a == &b says so for any type (see Addresses).

a + b at r joins text: new text holding the bytes of a and then of b. It is new text, so it goes in a region, as a line scan reads does, and its type is text@r. The region comes after the last part of a chain, first + " " + last at r, and names the region of the whole chain, or after parentheses, (first + " " + last)@r; the parts may be text of any regions, or text that lasts. A part that makes text of its own names its region with @, or in parentheses: "n = " + n.str()@r at r (see The region after a call). + on text without a region is an error that says to name one (joining text makes new text, which goes in a region …), and + joins only text: "n = " + 5 at r is an error. Text that mixes in values reads better written as text holding values, "n = {n}" at r.

task full_name(first: text, last: text, r: region) -> text@r
    <- first + " " + last at r
fi

task main()
    region r
    fixed name = full_name("Ada", "Lovelace", r)   ; text@r
    show(("<" + name + ">")@r)                  ; <Ada Lovelace>
    flex line = "".copy() at r
    each word in ["one", "two"]
        line = line + word + ";" at r                 ; one;two; — each join is new text in r
    fi
    show(line)
    show("Ada" + "!")                           ; error: joining text makes new text, which goes in a region …
fi

Each join makes new text in its region, and a region only frees memory when its block closes, so a loop that joins again and again keeps every step until then. A loop that runs long can build in a region declared inside it, and copy what it keeps out (see Copying into another region).

x.str() at r turns a value into text: new text in region r, written as show writes the value (2.5, 1e+20, true). The region can also follow as x.str()@r, or be given in the parentheses, x.str(r), and text(x) at r (or text(x)@r) is the same, named by the type the value becomes. These are the forms of every call that takes its region last (see The region after a call), so inside a + chain, whose at r is the chain's, a value turned into text names its own with @: "(" + .x.str()@r + ")" at r. Numbers, bools, text, pointers, and arrays, slices, tuples, and maybes of values with str have it built in, written as show writes them ([1, 2], { 4 }, 0x7ffd…), and so do shapes and kinds (Point { x: 1, y: 2 }, Circle(1.5)); the text is made once, at its full length, and text turned into text is a copy of it in r. A shape or a kind replaces its default with a method written str(r: region) -> text@r, and every one of these forms calls it, as show(x) does when it writes the value, and as an array of them does for each item. Such a method never changes the value (a method that does is another method, with another name), and is never priv, since the value is written wherever it is shown. A value without str (a value that can fail, or anything holding one, a shape among them unless it writes itself) is an error saying which part can't be written, and so is a str without a region. + never turns a value into text by itself: "n = " + 5 at r is an error that says to turn it into text first.

shape Point
    x: int
    y: int
    str(r: region) -> text@r
        <- "(" + .x.str()@r + ", " + .y.str(r) + ")" at r
    fi
fi

task main()
    region r
    show(42.str() at r)                          ; 42
    show(text(2.5) at r)                         ; 2.5, as show writes it
    fixed p = Point { x: 3, y: -4 }
    show("p = " + p.str(r) at r)                  ; p = (3, -4)
    show("p = ", p)                            ; the same line, with no region to name
    show("n = " + 5 at r)                        ; error: '+' joins text, but this value is int; turn it into text first …
fi

Values as JSON. x.json() at r (or x.json(r), x.json()@r, or to_json(x, r) in std/json) turns a value into standard RFC 8259 JSON text in region r. Numbers, booleans, and characters format according to JSON rules; text strings and characters are escaped and quoted with standard JSON escapes (\", \\, \n, \t, \r), and the other control characters as \u00XX. A float is written with the fewest digits that read back as the same number, 0.30000000000000004, and nan and the infinities, which JSON has no numbers for, as null. A key of a map that isn't text is written as text, {"1":"one"}. A pointer, a task, and a lambda have no JSON form, so a value holding one is an error, unless the shape holding it has its own json method. Arrays and slices serialize as [v1,v2,…], and maybe serializes as null when none or as its inner value. Map literals serialize directly as JSON objects, {"key":val,…}, and shapes serialize their public fields directly as JSON objects (omitting priv fields). A shape or kind can also define its own json(r: region) -> text@r method. In server development, std/web provides json_of(value, r) and json_map(map, r) to return an HTTP Response with application/json; charset=utf-8 directly without manual string interpolation.

Text holding values. A value in braces inside text, "n = {n}" at r, is turned into text as n.str() does and put in its place: "{name} is {age}" at r is new text in r, so it names its region as a join does, after the text ("…" at r) or with @ ("…"@r). The braces may hold any value that has str: a name, a call with text of its own ("{f("x")}"), arithmetic, a field, a plan B, even text holding values ("{"{n}"}", which takes the region of the text around it); spaces inside them don't count. The text is made once, at its full length: a number is written straight into it, and text is read where it is, so nothing is left behind in the region but the text itself (a value written by a shape's own str makes that text first). A value without str is the same error as its str would be. Text holding no value is a literal as always and needs no region, and \{ and \} write braces. In a join the text takes the join's region: "Dear " + "{name}," at r. Written without a region, it is an error that says to name one (text holding values is new text, so it names the region it goes in …); so are an unclosed {, a } alone, and empty braces, each saying how to write a brace instead.

shape Point
    x: int
    y: int
    str(r: region) -> text@r
        <- "({.x}, {.y})" at r
    fi
fi

task main()
    region r
    fixed name = "Ada"
    fixed n = 3
    show("{name} has {n} cats" at r)             ; Ada has 3 cats
    show("{n} * 2 = {n * 2}, big: {n > 2}"@r)     ; 3 * 2 = 6, big: true
    show("at {Point { x: n, y: -n }}" at r)       ; at (3, -3), from Point's str
    show("\{name\} is {name[0..1]}…" at r)          ; {name} is A…
    show("n = {n}")                             ; error: text holding values is new text, so it names the region it goes in …
fi
; greet the user
show("say \"hi\"")       ; say "hi"
show(2.0 / 3.0)            ; 0.666667
show(1 < 2 and not false)  ; true
show(1 == 1.0)             ; error: expected type int, got float

Shapes

Shapes group named fields, like C's structs. (Several values of one type are an array; see Arrays.)

shape Point
    x: int
    y: int
fi

fixed p = Point { x: 10, y: 20 }
show(p.x)

A shape literal must give every field exactly once, in any order: Point { y: 20, x: 10 } is the same value. A shape may also have no fields (Marker {}).

A field can name the value a literal that leaves it out gives it, written after its type. It is a value the compiler works out without running the program — a number, text, true, false, none, a variant, arithmetic on numbers and macros (size: int = 2 * WIDTH, worked out once), or a tuple, an array, or a literal of those — so a literal that leaves the field out costs no more than one that writes it. The value is read where the literal is, so each value made gets its own. A literal may still write the field, and a shape whose every field names one is written Job {}.

kind State
    Idle
    Busy
fi

shape Job
    name: text                   ; every literal gives this one
    tries: int = 0
    note: maybe text = none
    state: State = State.Idle
fi

fixed sweep = Job { name: "sweep" }              ; tries 0, note none, state Idle
fixed again = Job { name: "scrub", tries: 3 }    ; what it writes, and the rest
Job { tries: 1 }                                ; error: missing field 'name' in 'Job'

This is what lets a shape keep a field of its own and still be built anywhere: a priv field with no value to fall back on can only be given inside its own module, so only that module can build the shape at all, while one that names a value is left out elsewhere and starts there.

The value a field falls back on belongs to the shape's file, which reads it wherever the literal is, so it may give what only that file can, such as a private field of another of its shapes. A field of a type the shape takes, as value: T in Box<T>, is a different type in every value and so falls back on nothing, except that spare: maybe T = none may, since none fits a maybe of every type.

Read-only fields. A field marked read is set once, and after that only the alters methods of its own shape change it; any other code, in any file, only reads it. So what a shape keeps true of itself stays so wherever its values go — in another file, and in the file that declares it too, since the field belongs to the shape rather than the file. It is set once in one of two ways: a field with no value to fall back on is given by the literal that makes the value (a maybe one too, even when it is none), and a field with one starts at it, and no literal may give it. A field is priv or read, not both: a field every file reads is no file's secret.

shape Meter
    read label: text             ; given by the literal, then only Meter changes it
    read count: int = 0          ; starts at 0, and only Meter changes it

    alters tick()
        .count = .count + 1     ; ok: a method of Meter
    fi
fi

flex m = Meter { label: "door" }
m.tick()
show(m.label, " ", m.count)     ; door 1
m.count = 5                     ; error: 'count' is a read-only field of 'Meter', which only the alters methods of 'Meter' change
Meter { label: "x", count: 3 }  ; error: … falls back on its own value, so no literal gives it
m = Meter { label: "gate" }     ; ok: a whole new value, which the literal rules keep honest

Assigning is not the only change: calling an alters method on the field, assigning a field or an item of an array it holds, and adding to or taking from a slice it holds all change it, and all are refused outside the shape's methods. What a pointer or a slice in it points to lives in a region rather than in the field, so m.items[0] = 1 or m.next^.value = 1 is still a change to that memory, not to the field.

read names no word of its own: a field called read is written read: and a method read(…), so both still work.

Shared fields. A field marked shared belongs to the shape itself rather than to each value: there is one of it for the whole program, however many values of the shape there are, even none. It is read and changed only through the shape's name, Point.made (or geometry.Point.made from another file), never through a value, and a literal never gives it. Inside the shape's own methods it is written the same way, since it is no member of .this.

shape Ticket
    number: int
    shared prefix: text = "T-"          ; never changes
    shared read issued: int = 0        ; only Ticket's methods change it
    shared flex limit: int = 100      ; any code may change it
    priv shared flex last: int = 0    ; only this file sees it

    alters issue()
        Ticket.last = Ticket.last + 1
        .number = Ticket.last
        Ticket.issued = Ticket.issued + 1
    fi
fi

flex t = Ticket { number: 0 }
t.issue()
show(Ticket.prefix, t.number, " of ", Ticket.issued)   ; T-1 of 1
Ticket.limit = 50                ; ok: shared flex
Ticket.issued = 7                ; error: 'Ticket.issued' is a read-only shared field, which only the methods of 'Ticket' change
show(t.issued)                   ; error: 'issued' is a shared field of 'Ticket' … so it is read as 'Ticket.issued'
Ticket { number: 1, limit: 3 }   ; error: 'limit' is a shared field of 'Ticket' … so no literal gives it

In C each shared field is one variable of its own, given its first value where the program starts: a value of the shape is no larger for it, and reading one costs what reading a variable does. Since Point.made names the shape, no variable may be named like a shape, as none may be named like a kind. Counting how many values of a shape exist, rather than how many were made by one method, needs code that runs when a value is made and when it goes away, which is the events idea.

Shared methods. A method marked shared belongs to the shape or kind itself too: it is called on the type's name, Point.new(1, 2) (or geometry.Point.new(1, 2) from another file), never on a value, and has no value it is called on, so no .this, no .x, and no @this. It takes parameters, regions among them, and sends what it makes as a task does. Its usual job is making a value, so new, a keyword elsewhere, is the one keyword a method may be named, and only a shared one. Named without (…), Point.new is the method as a value, a (int, int) -> Point (see Tasks as values), and it takes decorators as a task does, and can be one.

shape Point
    x: int
    y: int
    shared read made: int = 0

    shared new(x: int, y: int) -> Point
        Point.made = Point.made + 1
        <- Point { x: x, y: y }
    fi

    moved(by: int) -> Point
        <- Point.new(.x + by, .y + by)
    fi
fi

kind Light
    Red
    Green

    shared start() -> Light
        <- Light.Red
    fi
fi

task main()
    fixed p = Point.new(1, 2)
    show(p.moved(1), " ", Point.made, " ", Light.start())   ; Point { x: 2, y: 3 } 2 Red
fi

In C a shared method is a function without the parameter that holds the value, so calling one costs what calling a task does.

Shapes and kinds can be used before they are declared. A type cannot contain itself by value, since its values would be infinitely large, but it can point to itself. A pointer always points to something, so the pointer to the next one must be a maybe: the last node has none. (A field's pointer names no region: a shape that holds pointers lives in one, written Node@r; see Memory and Regions.)

shape Node
    value: int
    next: maybe ^Node      ; ok: a pointer that can be none
fi

shape Loop
    inner: Loop            ; error: 'Loop' contains itself by value
fi

shape Chain
    next: ^Chain           ; error: 'Chain' can never be created: field 'next' needs an existing ^Chain
fi

shape Dir
    subs: [Dir]            ; ok: a new slice is empty, so it needs no Dir first
fi

Assigning to a field requires the variable to be a flex, or a flex parameter.

flex q = Point { x: 1, y: 2 }
q.x = 5                    ; ok
p.x = 5                    ; error: cannot assign to a field of immutable variable 'p'

Kinds

Kinds are tagged unions with exhaustive matching. Each case binds the variant's payload fields in order; the bindings are visible only inside that case, so different cases may reuse names. A kind value has no fields of its own (s.radius is an error): match is the way to read a payload.

kind Shape
    Circle(radius: float)
    Rectangle(width: float, height: float)
fi

task area(s: Shape) -> float
    match s
    case Circle r
        <- 3.14 * r * r
    case Rectangle w h
        <- w * h
    fi
fi

A match must have a case for every variant, each once; leaving one out is an error that names it. A last case _ covers every variant not matched above (it is an error when there are none left), and _ in place of a binding leaves that payload field unnamed.

match s
case Rectangle w _
    show(w)
case _
    show("not a rectangle")
fi

A variant can have no payload. Write its value as just its name, with or without parentheses; its case binds nothing.

kind Light
    Red
    Green
    Dimmed(level: int)
fi

fixed a = Red
fixed b = Green()          ; same as Green
fixed c = Dimmed(3)

A variant can also be named through its kind: Light.Red and Light.Dimmed(3) are the same values as Red and Dimmed(3), and a case form may be written the same way, case Light.Dimmed level (the kind it names must be the value's). It is how another module's variants are named when only their kind is brought in (see Modules). Since Light.Red reads like a field, no variable can be named like a kind.

A kind can have methods too, below its variants, which read the value as .this (see Methods). A match can also be the value itself, with each case giving one (see A match or a when as a value).

Methods

A shape or a kind can have methods: tasks of its own, written inside its block, below every field or variant, with no task before them. A method is called on a value, c.total(), and reads what that value holds with a dot: .count is its field and .cap() another of its methods. A name without a dot still means a variable or a task, so a field and a variable never clash.

shape Counter
    label: text
    count: int

    total() -> int
        <- .count
    fi

    alters step()
        .count = .count + 1
        .cap()
    fi

    priv alters cap()
        when .count > 3
            .count = 3
        fi
    fi
fi

flex c = Counter { label: "clicks", count: 0 }
c.step()
c.step()
show(c.total())            ; 2

A method that changes the value it is called on says alters before its name; one that assigns a member without it is refused, with the fix named. Only a place that can change may call it: a flex variable, a field or item of one, an item of a slice, or what a pointer points to (p^.step()), reached from a flex name: nothing changes through a fixed one, at any depth, so p^.step() with a fixed p is refused too. The compiler gives the method that place, so the change lands where the caller keeps the value, as a flex& parameter is given one. A fixed, a parameter not written flex or flex&, a result, and a value made on the spot never change:

fixed steady = Counter { label: "once", count: 1 }
show(steady.total())       ; fine: total changes nothing
steady.step()              ; error: 'Counter.step' changes the value it is called on, so it needs a place
                           ;   that can change, and 'steady' never does (declared at 1:7); a value declared
                           ;   with 'flex' can

task bump(c: Counter)
    c.step()               ; error: ... and 'c' is a parameter, which never does; write 'flex c: Counter'
fi                         ;   to change the task's own copy, or 'flex& c: Counter' to change the caller's

The whole value is .this. A kind has no fields, so its methods read .this and take it apart with match, and an alters method moves the value to another variant with .this = Other. (A shape's methods have .this too.)

kind Light
    Red
    Green
    Blinking(times: int)

    label() -> text
        match .this
        case Red
            <- "red"
        case Green
            <- "green"
        case Blinking _
            <- "blinking"
        fi
    fi

    alters flip()
        match .this
        case Red
            .this = Green
        case _
            .this = Red
        fi
    fi

    tell(prefix: text) -> nothing
        show(prefix)
        show(.label())
    fi
fi

flex light = Red
light.flip()
light.tell("now:")         ; now:, then green

Inside a kind, a variant's values and a method's parameters are written alike: Blinking(times: int) and tell(prefix: text). A member is a method when it says alters, names what it sends (-> text), takes nothing (label()), or comes after another method. That leaves one case, a kind's first method that takes values and sends nothing, which writes -> nothing, as tell would if it came first; the error for a method read as a variant says so.

Arrays

An array holds a set number of values of one type, written [type#count]. A literal lists the values, or repeats one: [0#8] is eight zeros, the value computed once and copied. xs[i] is an item, counting from 0, and xs.len is how many items there are. An array is a value, like a shape: assigning or passing it copies every item, and an item can be assigned where a field could be, as in a flex.

fixed scores: [int#4] = [90, 72, 85, 60]
show(scores[2])             ; 85
show(scores.len)            ; 4
flex grid = [[0#3]#2]     ; [[int#3]#2]
grid[1][2] = 5
flex copy = scores          ; a copy: changing it leaves scores as it was
copy[0] = 0
scores[0] = 0               ; error: cannot assign to an item of immutable variable 'scores'
each score in scores
    show(score)
fi
show(scores[#9], " ", scores[#1])    ; none { 72 }: a safe index gives a maybe
flex i = 0
loop scores[#i] is {score}   ; stops at the first none, past the end
    i = i + 1
fi

Tuples

A tuple holds two or more values together, each of its own type: (text, int), written as a value ("Ada", 36). t.0, t.1, … read its items by position, from 0, each written as a plain number (t.01 is an error). t.#0 reads an item in a maybe, as xs[#i] reads an array's, and past the tuple's end it is none, which takes its maybe type from where it goes: a binding's type (fixed m: maybe int = t.#5), or a plan B's value, which it always gives (t.#5 { 0 } is 0); show(t.#5) writes none. A tuple is a value, like a shape, and its items never change: a flex tuple takes a whole new tuple, but no item can be assigned, or changed by a method that alters the value it is called on. Names take a tuple apart, as a list of names takes a task's several results.

is can test what t.#k finds, read from a tuple with a name. t.#5 is none holds when there is no item 5. t.#2 is maybe int {x} holds when item 2 is an int, and gives it as x; t.#2 is maybe int x holds when it is an int or missing, and x is the read, a maybe int. t.#2? is maybe int tests the type alone: the ? asks for the type itself, not a value, and after it the reads of t.#2 in the same condition and its block are maybe ints, as in t.#2? is maybe int and t.#2 is {x}. The type is one the program names (not T of a task taking types), matched as it is, regions aside: maybe text {s} takes a text@r, and s keeps its region. Each answer is known when compiling, so nothing is tested when the program runs, a branch that can't run is left out, and so are the branches after one that always holds and the parts of a condition after one that never does, which may read what it guards against; this is what makes them useful on the tuple of a group's values, whose item 2 has a different type, or none, in each use, and on an item holding a T, which each copy of the task settles. What such a test takes from an item whose type each use gives may be in a region of the caller's, so it stays inside the task: maybe text {s} gives a text that can't be sent back or kept outside, even where the test names a region, as in maybe text@r {s}, unless copied into a region, as in s.copy()@r. t.#5 is {x} past the end is still an error: it never matches, and x would have no type.

task third<P...: frozen & shown>(args: P...)
    fixed t = (args...) { <- }
    when t.#2 is maybe int {n}
        show("an int, doubled ", n * 2)
    else when t.#2 is none
        show("no third")
    fi
fi
task divide(a: int, b: int) -> (int, int)
    <- (a / b, a % b)
fi

task main()
    fixed pair: (text, int) = ("Ada", 36)
    show(pair.0)                         ; Ada
    show(pair.1)                         ; 36
    flex point = (1, (2.5, "deep"))      ; (int, (float, text))
    show(point.1.1)                      ; deep
    point = (2, (3.5, "other"))          ; a flex tuple takes a whole new tuple
    fixed q, r = divide(17, 5)            ; unpacks the tuple: 3 and 2
    fixed (whole, left) = divide(9, 4)    ; the same, in parentheses
    fixed first, (half, label) = point    ; a nested part, in parentheses
    flex x = 0
    flex y = 0
    x, y = divide(20, 6)                 ; into flex variables of the items' types
    (y, x) = (x, y)                      ; swapped
    point.0 = 5                          ; error: a tuple's items never change; …
    show(pair ~= ("Ada", 36))            ; true: item by item
fi

Types a declaration takes

A task can take types as well as values: task first<T>(xs: [T]@r) -> maybe T works for a slice of anything. The names in < > after the task's name are the types it takes, and T in its header and body means the type each call gives it. The body is checked once, with T standing for that type, and the compiler then makes one task for each set of types the program uses it with, so a call costs nothing more than an ordinary one. A shape and a kind take types the same way (Shapes that take types, Kinds that take types).

A word after a type's : says what every type given for it must allow, and so what the body may do with its values. Without a word, values of that type can only be stored, passed, and sent.

Word What the body may do Types that fit
frozen put a value in a tuple, whose items never change numbers, bools, text, tuples of them, and shapes and kinds whose fields are all such
same == and !=, each type by its own rule numbers, bools, text, pointers, slices, arrays, tuples, shapes, and kinds whose variants carry none of T ~ E
similar ~= and <>, item by item numbers, bools, text, pointers, and arrays, slices, tuples, maybes, shapes, and kinds of them
shown show, str anything show writes: numbers, bools, text, pointers, arrays, slices, tuples, and maybes of such, and shapes and kinds, except those holding a value that can fail without a str of their own
ordered <, >, <=, >= numbers, chars, and text

Each word is one thing the body may do, and & asks for several: <T: same & shown> may compare its values and write them, and only a type that allows both fits. A word is written for everything the body relies on, even where every type that fits one fits another today, so a declaration says what it needs, and a word given its own meaning later (by a shape of your own, say) changes nothing that is already written. (same, similar, and frozen are on the May Waste list, which keeps an eye on what may not earn its place.) The one word that means another is ordered, which means same, since what has an order can be compared; writing both is an error that says to keep ordered. ordered doesn't mean shown or frozen: a task that compares and prints its values says <T: ordered & shown>, and one that leaves out a word it uses is an error that says which to add, as in T is only 'ordered' here; require it too, as in '<T: ordered & shown>'. No word asks a type for arithmetic: a task that adds its values takes numbers. shown allows every type show writes, a shape or kind with its own str or its default one, so a task that prints its values takes almost any of them. Besides the words, the name of a pattern asks every type given to follow it, <T: Shaped>, and lets the body call its methods.

task first<T>(xs: [T]@r) -> maybe T
    each v in xs
        <- v
    fi
    <- none
fi

task biggest<T: ordered>(a: T, b: T) -> T
    when a > b
        <- a
    fi
    <- b
fi

task empty<T>() -> maybe T
    <- none
fi

task main()
    region r
    fixed ns: [int]@r = new [int]([4, 5]) at r
    flex names: [text@r]@r = new [text]() at r
    names.add("Ada".copy() at r)
    show(first(ns))                      ; { 4 } — the argument says T is int
    show(first<text@r>(names))            ; { "Ada" } — or the call writes it out
    show(biggest(2, 5))                   ; 5
    show(biggest(1.5, 0.5))               ; 1.5 — the same task, made for float too
    fixed blank: maybe int = empty()    ; the place the value goes says T is int
    fixed fallback: text = empty() { "none" }
    show(biggest("fig", "pear"))          ; pear — text has an order too
    show(biggest(true, false))            ; error: task 'biggest' takes a type that is 'ordered' …
fi

Shapes that take types

A shape takes types in the same < >, with the same words: shape Box<T>, shape Pair<A: frozen, B: frozen>. Its fields are read once, with T standing for the type each value of it is given, and the compiler makes one shape for each set of types the program uses, so Box<int> is a struct holding an int64_t and nothing more.

The types given name no region, as a shape's fields don't: they are in the region of the value holding them, which the whole type names. So Box<text>@r is a box whose text is in r, one shape serves every region, and Box<int> names no region at all, since it holds nothing in one.

shape Box<T>
    value: T
fi

; A field may name the shape again, with the types it takes, behind a pointer.
shape Node<T>
    value: T
    next: maybe ^Node<T>
fi

task valueOf<T>(b: Box<T>@r) -> T
    <- b.value
fi

task main()
    region r
    fixed count = Box { value: 1 }          ; Box<int>: the value says which type
    fixed empty = Box<maybe int> { value: none }  ; written, since none says nothing
    fixed kept: Box<text>@r = Box { value: "ada".copy() at r }
    show(valueOf(count))                   ; 1
    show(kept.value)                       ; ada
    fixed tail: ^Node<int>@r = new Node<int>(Node { value: 3, next: none }) at r
    fixed head: ^Node<int>@r = new Node<int>(Node { value: 2, next: tail }) at r
    show(head^.value)                      ; 2
fi

Kinds that take types

A kind takes types as a shape does, in the same < > with the same words: kind Option<T>, kind Either<A, B>. Its variants are read once, with T standing for the type each value of it is given, and the compiler makes one kind for each set of types the program uses, so Option<int> costs what a kind carrying an int does.

kind Option<T>
    Some(value: T)
    None

    orElse(fallback: T) -> T
        <- match .this
        case Some v = v
        case None = fallback
        fi
    fi
fi

kind Either<A, B>
    Left(a: A)
    Right(b: B)
fi

task half(n: int) -> Option<int>
    when n % 2 == 0
        <- Option.Some(n / 2)        ; Option<int>: the value says which type
    fi
    <- Option.None                     ; the result's type says it
fi

task main()
    show(half(8), " ", half(7))         ; Some(4) None
    fixed empty: Option<text> = Option.None ; the variable's type says it
    fixed written = Option<float>.None     ; written, since nothing else does
    fixed right = Either<int, text>.Right("r")
    fixed many = [Option.Some(1), Option.None]  ; the value beside it says it
    show(many, " ", right.type)            ; [Some(1), None] Either<int, text>
    show(half(7).orElse(0), " ", empty, " ", written)  ; 0 None None
    match half(8)
    case Some v                          ; v: int
        show(v)
    case None
        show("odd")
    fi
    fixed lost = Option.None              ; error: cannot infer the type of 'lost'
fi

Kinds that imitate kinds

A kind can have every variant of another kind, written once where that kind is: kind OrderError::ReadError has each variant of ReadError, with the same name and carrying the same values, before the variants it adds below. It imitates the other kind. Only the variants come along: the other kind's methods, shared or not, stay its own, and the new kind may have its own.

kind ReadError
    Empty
    BadDigit(column: int)
fi

kind OrderError::ReadError
    TooMany(count: int)
fi

task read_count(line: text) -> int ~ ReadError
    when line.len == 0
        <- fail Empty
    fi
    <- line.len
fi

task order(line: text) -> int ~ OrderError
    fixed count = read_count(line) { failure e
        <- fail e                       ; a ReadError goes out as an OrderError
    }
    when count > 3
        <- fail TooMany(count)
    fi
    <- count
fi

task said(e: OrderError) -> int
    <- match e                          ; every variant, its ReadError ones too
    case Empty = 0
    case BadDigit column = column
    case TooMany count = count
    fi
fi

task main()
    fixed first: OrderError = Empty         ; a ReadError where an OrderError goes
    fixed second = OrderError.BadDigit(2)   ; named through the kind that has it
    show(first, " ", second, " ", first == Empty)   ; Empty BadDigit(2) true
    each line in ["ab", "", "abcde"]
        match order(line)
        case {count}
            show("ordered ", count)         ; ordered 2
        case failure e
            show(e, " ", said(e))           ; Empty 0, then TooMany(5) 5
        fi
    fi
fi

Patterns

A pattern says what a shape or kind has, so that code can rely on it whichever shape it is given. It is written outside every task, shape, and kind, at the start of a line, as pattern Name … fi, and holds fields and methods. A shape or kind follows it by naming it after ::, as in shape Square::Shaped, and a task taking types asks for it after a type's :, as in task total<T: Shaped>(…), as it asks for a word like shown.

pattern Shaped
    name: text
    area() -> float
    start(), stop()                 ; two headers alone: each shape writes them
    describe(r: region) -> text@r   ; a default, with its body
        <- "{.name} of area {.area()}" at r
    fi
    larger(other: this) -> bool     ; this: the type following the pattern
        <- .area() > other.area()
    fi
fi

shape Square::Shaped
    name: text
    side: float

    area() -> float
        <- .side * .side
    fi
    start()
        show("square starts")
    fi
    stop()
        show("square stops")
    fi
fi

shape Circle::Shaped
    name: text
    radius: float

    area() -> float
        <- 3.0 * .radius * .radius
    fi
    start()
        show("circle starts")
    fi
    stop()
        show("circle stops")
    fi
    describe(r: region) -> text@r   ; its own, in place of the default
        <- "a circle of radius {.radius}" at r
    fi
fi

task total<T: Shaped>(xs: [T]@r) -> float   ; any type that follows Shaped
    flex sum = 0.0
    each x in xs
        sum += x.area()
    fi
    <- sum
fi

task main()
    region r default
    fixed s = Square { name: "tile", side: 2.0 }
    fixed c = Circle { name: "ring", radius: 1.0 }
    show(s.describe(r))                         ; tile of area 4
    show(c.describe(r))                         ; a circle of radius 1
    show(s.larger(Square { name: "dot", side: 1.0 }))   ; true
    c.start()                                   ; circle starts
    show(total([s, s, ...] at r), " ", total([c, ...] at r))   ; 8 3
fi

Macros

A macro names a value the compiler computes. It is written outside every task, shape, and kind, at the start of a line, as #NAME = value, with # before its name, and its type is its value's: no type is written. Each use of the name is that value, and nothing can change it. In C, a macro is a #define holding the value, and each use names it.

#P = 3.14159
#TAU = P * 2.0          ; 6.28318, a float
#SIZE = 4
#LAST = SIZE - 1        ; 3, an int
#NAME = "vela"
#TITLE = NAME + "!"     ; "vela!", text
#FIRST = 'a'
#BIG = SIZE > 2         ; true
#SMALL = unit(200)
priv #SECRET = 42       ; only this file names it

task main()
    show(TAU, " ", TITLE, " ", LAST)   ; 6.28318 vela! 3
    fixed xs = [0#SIZE]                 ; [0, 0, 0, 0]
    flex b: unit = SIZE                ; an int macro is a unit where one is expected, as 4 is
fi

A value is anything the compiler can work out: literals of numbers, units, chars, bools, and text; other macros, declared above or below; the operators on them (+ - * / %, comparisons, and, or, not, and + joining text); and the conversions int(…), float(…), unit(…), and char(…). They follow the rules they follow in a task, and what would stop the program when it runs stops the compiler instead: a division by zero, an int that overflows, a char or a unit outside 0..255, and a float that is no number. A call, a variable, an array, text holding values ("{NAME}!"), and none are made when the program runs, so no macro can be one; nor can a macro be computed from itself, through others or not.

A macro is a name of its file, as a task is: another file names it m.NAME, or NAME after include m (NAME), and priv #NAME = … keeps it to its own file (see Modules). The # is written only where the macro is declared; a use is the name alone. No variable takes a macro's name, so P always means the macro. The # of an array's count ([int#4], [0#4]) and of a safe index (xs[#i]) is always inside brackets, so the two never meet. A macro is a value, not a place: P = 3.0 and &P are errors. Macros that take values, as C's #define MAX(a, b) does, are an idea for later (see Macros that take values).

Type aliases

A type alias gives a type another name. It is written outside every task, shape, and kind, at the start of a line, as alias Name => type, and in each place the name is written, the type is: an alias is no type of its own, so a value of it is a value of that type and goes wherever one goes. A long type is read once, where it is named, and the places that use it say what it is for: [SampleTask#4] is plainly an array of four tasks, where [(int) -> int#4] has to be read twice.

alias SampleTask => (int) -> int
alias Cents => int
alias Prices => [Cents]

task add_one(n: int) -> int
    <- n + 1
fi

task total(prices: Prices@r) -> Cents
    flex sum = 0
    each p in prices
        sum += p
    fi
    <- sum
fi

task main()
    region r default
    fixed tasks: [SampleTask#4] = [add_one, add_one, add_one, add_one]   ; an array of four tasks
    flex later: [SampleTask]@r = new [SampleTask]                         ; a slice of tasks
    later.add(add_one)
    fixed prices: Prices@r = [250, 199, ...]
    fixed cents: int = total(prices)                                       ; a Cents is an int
    show(tasks[3](1), " ", later[0](5), " ", cents)                        ; 2 6 449
fi

A region written after an alias is the region of the type it stands for, as if that type were written there: Prices@r is [int]@r. An alias of a type that lives in no region takes none, so Cents@r is an error, as int@r is. The type may name shapes, kinds, and other aliases, declared above or below, and give a shape its types (alias Counts => Box<int>); an alias takes no types itself, and none stands for itself, through others or not.

An alias is a name of its file, as a macro is: another file names it m.Name, or Name after include m (Name), and priv alias Name => … keeps it to its own file (see Modules). It names a type and nothing else: a shape's value is written with the shape's own name, Point { x: 1 }, and Cents(2.5) is no conversion, where int(2.5) is. The checker sees only the type an alias stands for, so a message names that type: fixed c: Cents = "a" says expected type int, got text.

Tasks as values

A task's name without () is a value: the task itself, which a variable, a parameter, a field, an item, or a result can hold, and a call runs. Its type is written as the task's header without the names, (int, text) -> int, and a task that sends nothing is (int) -> nothing: the arrow is always written, since (int) alone is int in parentheses. A task that takes nothing is () -> int.

task square(n: int) -> int
    <- n * n
fi

task add(a: int, b: int) -> int
    <- a + b
fi

task apply(f: (int) -> int, x: int) -> int
    <- f(x)                        ; calls the task f holds
fi

task adder() -> (int, int) -> int
    <- add                         ; the task itself, not a call of it
fi

shape Op
    name: text
    run: (int, int) -> int
fi

task main()
    show(apply(square, 7))          ; 49
    fixed f = square                 ; f: (int) -> int
    show(f(3), " ", f == square)    ; 9 true
    fixed op = Op { name: "add", run: add }
    show(op.run(4, 5))              ; 9: a field that holds a task, called
    show(adder()(6, 7))             ; 13: the task a call gives, called
    flex later: maybe (int) -> int = none
    later = square
    when later is {g}
        show(g(2))                  ; 4
    fi
    show(f)                         ; task (int) -> int
fi

f(x) calls the task the variable or parameter f holds, and op.run(x) the one a field holds, where the value has no method of that name; xs[0](x), adder()(x, y), and (f)(x) call the task any value holds, and Shape.f(x) the one a shared field holds. The call is checked as a named task's is, against the type. A module's task is a value too, m.square, and so is a shared method, named on its shape or kind without (…): fixed make = Point.new holds a (int, int) -> Point, and geometry.Point.new names one of another module. No variable holding a task may be named like a task, since name(…) would call that task instead.

Two task values are == when they hold the same task, and ~= means the same. A task value is written as its type, task (int) -> int, since it can't say which task it holds, and its .type is (int) -> int. A task value holds nothing in a region, so it goes anywhere: a tuple, an array, a slice, a map's value, a kind's variant, a shared field (whose first value can be a task), and a maybe.

The regions in a task's type are its own, as the regions in a header are (see Memory and Regions): (text@r) -> text@r takes text of any region and sends text of the same one, and (text) -> text takes text that lasts as long as the program. So a task of header task head(t: text@r) -> text@r is a (text@r) -> text@r whatever the names, and not a (text) -> text. What a task sends lives in regions its parameters name, as in a header.

Places and regions. A task's type says how each parameter is given, as its header does: a value as its type alone, the caller's place as flex& T or fixed& T, and a region as @r, named so that what the task sends can live there. So task bump(flex& n: int) is a (flex& int) -> nothing, task squares(r: region, n: int) -> [int]@r a (@r, int) -> [int]@r, and task label(n: int, r: region) -> text@r, which takes its region last, a (int, @r) -> text@r. A copy, flex n: int or fixed n: int, is a value to the caller, so its type writes int.

A value the task changes through. A slice, a map and a pointer, and what holds one, are twofold: a copy names the caller's items. So a task taking flex xs: [int]@r changes what the caller's slice holds, and its type says so: (flex [int]@r) -> nothing. It goes only where a type says flex for that value, and a call through such a value is checked as a call of a task with a flex parameter is: nothing a fixed name owns is given there (see What a fixed name owns). A task that only reads, xs: [int]@r, goes there too, since it changes less, but never the other way. Before a whole value, copied whole, flex says nothing a caller sees, so (flex int) -> int is (int) -> int, which task bump(flex n: int) is; fixed, which a plain parameter already is, reads as nothing too. The same holds for lambdas, [@r](flex [int]@r) -> nothing, and for wrappers. bind says it for a task taking the value as bind, (bind [int]@r) -> nothing: such a task changes the items as a flex one may, so it also goes where the type says flex, and one that only reads goes where either is said, never the other way. Before a whole value bind reads as nothing, as flex does, and bind& before a type with no contents reads as fixed&, which it acts as.

task zero(flex xs: [int]@r)
    xs[0] = 0
fi

task apply(f: (flex [int]@r) -> nothing, flex xs: [int]@r)
    f(xs)
fi

task main()
    region r default
    flex xs = [1, 2, ...]
    apply(zero, xs)
    show(xs)                        ; [0, 2]
    fixed kept = [1, 2, ...]
    fixed f: (flex [int]@r) -> nothing = zero
    f(kept)                         ; error: 'f' changes what its first value reaches, so it can't
                                    ;   be given what fixed 'kept' owns; give it a copy, …
fi

And fixed plain: ([int]@r) -> nothing = zero is an error that names the type to write: … this task changes what a twofold value it is given reaches, a slice, a map or a pointer or what holds one, so it goes only where the type says 'flex' for that value, as in '(flex [int]@r) -> nothing'.

task bump(flex& n: int)
    n = n + 1
fi

task squares(r: region, n: int) -> [int]@r
    flex made = new [int]() at r
    each i in 1..n + 1
        made.add(i * i)
    fi
    <- made
fi

task twice(f: (flex& int) -> nothing, flex& v: int)
    f(&v)
    f(&v)
fi

task main()
    flex count = 1
    twice(bump, &count)
    show(count)                     ; 3
    region kept
    fixed g = squares                ; g: (@r, int) -> [int]@r
    show(g(kept, 3))                ; [1, 4, 9]
fi

A call through a value is checked as a named call is: it marks each place it gives with &, gives a place the call changes only once, never an item of a slice or a value made on the spot, and gives a region the caller can allocate in, in which what the task sends then lives. The regions are named in the parentheses, g(kept, 3); g(3) at kept is an error, since a region after a call is the region of what a named task sends, and a task value's type is the only thing that says where that is. A task value holds no place and no region: it is given them at each call, as a named task is, so it goes anywhere a task value goes. Two types that differ only in how a parameter is given are different types, (flex& int) -> nothing is not (int) -> nothing (but a task that only reads a slice goes where one that changes it is expected, see above), and a region's name is the type's own, as any of its regions' is: (@r, int) -> [int]@r and (@s, int) -> [int]@s are one type.

So a task taking types can't find one of them in a region a task type takes, or names in what it takes. In task logged<R>(next: (@a, int) -> R, a: region, n: int) -> R, given squares, R would be [int] in the region the body of logged gives next, which no call says (the a in (@a, int) is the type's own, not the parameter a), so the use is an error that says to write the type out: task logged(next: (@a, int) -> [int]@a, a: region, n: int) -> [int]@a, whose body is then checked to send what it says.

A task is a value when it sends one value or nothing. A task that takes types or sends several values can't be a value yet, and neither can a method of every value (Point.area) or a builtin (show, scan); a shared method follows a task's rules. In C a task value is a pointer to the task's function, taking a place as a pointer (to a const place for fixed&) and a region as the region; a task whose C takes a large value otherwise than its type does (a flex copy, or a task of the standard library) is given a small function that takes it the type's way.

Lambdas

A lambda is a task with no name, written where its value goes: its header without task and a name, (x: int) -> int, then its body and fi. The body goes on the lines after the header, or on the header's own line, where it may start with <- and must end with fi on that line. Its value is of the type its header gives, (int) -> int, as a named task's is. The arrow is always written, and a lambda that sends nothing says -> nothing.

task apply(f: (int) -> int, x: int) -> int
    <- f(x)
fi

task main()
    fixed double = (x: int) -> int <- x * 2 fi
    show(double(21))                              ; 42
    show(apply((n: int) -> int
        fixed next = n + 1
        <- next * next
    fi, 3))                                       ; 16
    fixed say = (t: text) -> nothing show("say ", t) fi
    say("hi")                                     ; say hi
    show(((n: int) -> int <- n - 1 fi)(10))       ; 9: called where it is written
fi

A plain lambda takes nothing from the code around it: its body sees its parameters, its own variables, and what the file declares (tasks, shapes, kinds, macros, shared fields), but no variable or region of the task it is written in, and in a method no .this. Give what it needs as a parameter, or write a lambda that keeps it (below); naming a variable of the code around a plain lambda is an error that says so. So a plain lambda holds nothing in a region and costs what a named task does: in C it is a function of the file, and its value a pointer to it.

Its parameters are those of a header: plain, flex, fixed, the caller's place (flex&, fixed&), and regions it takes (r: region, or r: region default), so (flex& v: int) -> nothing v = v * 2 fi is a (flex& int) -> nothing (see Places and regions). It sends one value or nothing; the regions in its header are its own, as a header's are, so (t: text@r) -> text@r <- t[0..1] fi takes text of any region. A lambda can't be written inside a task or method that takes types yet.

One-line lambdas

Where the place a lambda goes already says what it takes and sends, it is written short: its parameters' names, ->, and one expression, its value, with no types, no <- and no fi. One parameter may go without parentheses, x -> x * 2; several, or none, go in them, (a, b) -> a + b, () -> 42; and _ names one it doesn't use, _ -> 0, (_, _) -> 1. It is a lambda as any other, a task of the file, so it takes nothing from the code around it, and written after [@r] (or [flex @r], and the places it keeps) it keeps what it uses, as below: [@r] x -> x + offset.

task apply(f: (int) -> int, x: int) -> int
    <- f(x)
fi

task first<T>(xs: [T#3], f: (T) -> bool) -> maybe T
    each x in xs
        when f(x)
            <- x
        fi
    fi
    <- none
fi

task main()
    show(apply(x -> x * 2, 21))                   ; 42: f says it takes an int and sends one
    fixed add: (int, int) -> int = (a, b) -> a + b
    show(add(3, 4))                               ; 7
    show(first(["a", "bb", "ccc"], w -> w.len > 1) { "none" })   ; bb: T is text, from the array
    fixed answer = () -> 42                       ; takes nothing: its type is () -> int
    show(((x) -> x - 1)(10))                      ; 9: the value given says what x is
fi

Where its types come from. The parameter types and the result are those of the task type the place asks for: a task's parameter in a call or a pipe (in a task that takes types, once the other values have said what those types are, so T above is text); a variable written with its type; a shape's field in a literal; a task's <- when the task sends a task; and, for one called where it is written, the values the call gives. Where the place says nothing of its result, the result is the type of the expression. One that takes nothing needs no place at all: fixed answer = () -> 42 is a () -> int, and where a place asks for a () -> int8 the same lambda sends an int8. Anywhere else, fixed double = x -> x * 2 say, nothing says what x is, and the checker says so, asking for a full lambda or a type for the variable; it never guesses.

Where it ends. Its body is one expression, read as any other, pipes and all: it ends at a , or a closing bracket around it, or with its line. A body that needs statements, or a lambda whose types no place gives, is written in full. () -> followed by a type and a body on the line, as in () -> int <- 42 fi, or by nothing, is a full lambda's header; followed by a value, as in () -> 42, it is a one-line lambda. As every lambda, one can't be written inside a task or method that takes types yet, and one as an item of an array has no place saying its types.

Lambdas that keep what they use

A lambda written after [@r] keeps what it uses of the code around it in region r. When it is made, it copies each variable of the code around it that its body uses into a block of r (unless it keeps it as a place, below), and reads those copies where they are kept for as long as it lives. Its type says where, [@r](int) -> int, so it can't outlive r, as a ^T@r can't. Only the region is written, never what the lambda uses, so a task asking for a [@r](int) -> int takes any lambda kept in r.

task counter(r: region, start: int) -> [flex @r]() -> int
    flex n = start
    <- [flex @r]() -> int
        n = n + 1                                 ; its own copy of n, kept in r
        <- n
    fi
fi

task apply(f: [@r](int) -> int, x: int) -> int
    <- f(x)
fi

task double(x: int) -> int
    <- x * 2
fi

task main()
    region r
    flex next = counter(r, 10)
    show(next(), " ", next())                     ; 11 12
    fixed step = 3
    fixed plus = [@r](x: int) -> int <- x + step fi
    show(apply(plus, 1), " ", apply(double, 4))   ; 4 8
fi

What it keeps. A copy of each variable it uses: numbers, shapes, arrays, task values, and the handles of slices, maps, pointers and text. The variables themselves never change through the lambda, unless it keeps one as a place (below). What lives in r (a slice's items, what a pointer points to, text in r) is used where it is, so a [flex @r] lambda writing an item changes the one the code around it sees, as writing through any pointer does; a [@r] lambda's copies never change, so nothing is written through them. The copy of a slice shares its items but not its length, so a lambda doesn't add to or del from a slice it keeps a copy of: it makes one of its own, takes one as a flex& parameter, or keeps the slice as a place, [flex& xs, @s]. A value in another region is refused, since nothing in r points into another region (the rule a Person@r follows): copy it into r first, fixed mine = xs.copy() at r, and use that. The lambda sees r itself, so it may allocate there, and its types may name it, [@r](int) -> [int]@r; it sees no other region of the code around it but those a place it keeps is in, and in a method no .this or .x, so it keeps a variable of a member instead, fixed k = .n. What it uses is all it keeps, and its own names, its regions' too, are never those of the code around it.

Changing its copies. A [@r] lambda only reads its copies; [flex @r] changes them, as n = n + 1 does above, and a [@r] lambda that tries is told so. Every copy of a [flex @r] value would share its one block, so it is never copied: it is held in a flex variable, called, given as a place to a flex& f: [flex @r]() -> int parameter, named by a reference, flex& g = &next, or kept as a place by another lambda, [flex& next, @s]; it is never bound with fixed, given as a value or for a type a task takes, kept as a copy by another lambda, or held inside a tuple, a slice or a maybe, and it is called only through a place that changes, never through a fixed&. A [@r] lambda goes where a [flex @r] is expected, never the other way.

Changing only what copies share. [bind @r] changes only what every copy of it shares: the items of a slice it keeps a copy of, a map, what a pointer points to. It never assigns a copy as a whole, as a bind name doesn't, and its own copy of anything else, a number, a shape, an array, it only reads; one that changes those is a [flex @r] lambda, and a [bind @r] one that tries is told so. Since a copy of it would change only what the first one changes, it is copied as a slice is: bound with fixed, given as a value, held in a field, kept by another lambda. Calling it changes what it shares, as xs[0] = 1 changes a slice's items, so it is called through a flex or bind name, never a fixed one, and it can't keep a copy of what a fixed name owns. A [@r] lambda goes where a [bind @r] is expected, and a [bind @r] one where a [flex @r] is, never the other way (see safety).

Plain tasks and these. A task, a task value, or a plain lambda goes where a [@r](…) is expected, through a small function the compiler writes: a task or a plain lambda written there is kept where the program keeps it, and a task value held in a variable in a block of r, so a call that names r by no other value can't keep one (keep it first, fixed kept: [@r](int) -> int = plain). A lambda that keeps what it uses never goes where a plain task's type is expected: that type names no region, so the value could outlive r. Such a lambda isn't compared, copied with copy, or given as a wrapper's own value yet. A stop inside any lambda says where it is, at 6:39 in a lambda.

In a field. A field holds one as f: [@this](int) -> int: this is the region of the value holding it, so the lambda keeps what it uses there, and the value names that region, as a value holding text in a region does, fixed op: Op@r = Op { f: [@r] n -> n * k }, called as op.f(3). A value in no region holds only a plain task or lambda there, as a text field of one holds lasting text, and a plain (int) -> int field stays plain. A shared field, which lives in no value, holds a task value instead. The iterators of std/collections hold what their makers are given this way.

A field that changes things. A field holds a [bind @this](…) lambda as it holds a [@this] one, since it changes only what its copies share, and the value holding it is copied as any other; it is called through a value that changes, flex t or bind t, as t.put(2). A [flex @this](…) lambda changes its own copies, so a shape holding one, in a field or in a shape of a field, is held as such a lambda is: in a flex variable, given as a place, flex& c: Counter@r, or, to read it, fixed& c, made by a task that sends it, and never copied, bound with fixed, or held in a tuple, a slice, a maybe, or another value's variant. Its fields are read and its methods called where it is, c.start, c.peek(); its lambda is called there, c.next(), never taken out, and only through a place that changes, so a method calling it is alters. A variant holds none, since a match takes its values out as copies.

shape Counter
    start: int
    next: [flex @this]() -> int
fi

task counter(start: int, r: region) -> Counter@r
    flex n = start
    <- Counter { start: start, next: [flex @r]() -> int
        n = n + 1
        <- n
    fi }
fi

task main()
    region r
    flex c = counter(10, r)
    show(c.next(), " ", c.next())                 ; 11 12
fi

Keeping a variable as a place. A variable named in the list beside the region is kept as the variable itself, not a copy: flex& count changes it, bind& xs changes its contents, and fixed& big reads it where it is, with no copy of a large value, the words a parameter and a reference use. The lambda keeps a pointer to the variable, so it must end first: the region it is kept in is declared after the variable, in its block or a block inside it, and so ends first, and the lambda with it. A region given to the task, one declared before the variable or in a block around it, or one a lambda around this one keeps what it uses in, is refused, so such a lambda never leaves its task. A variable may be a flex& or fixed& parameter or a reference, whose place outlives the body; fields and items aren't kept so. The lambda sees the regions a place it keeps is in, which outlive it, so a slice kept with flex& grows itself, and the code around it keeps using the variable as usual. A flex& place changes the variable without [flex @r], which only says the lambda changes its copies. A listed place the body never uses is a warning.

task total(xs: [int#3]) -> int
    flex sum = 0
    region r                                      ; after sum, so it ends first
    fixed add = [flex& sum, @r](x: int) -> nothing sum = sum + x fi
    each x in xs
        add(x)
    fi
    <- sum
fi

task main()
    show(total([1, 2, 3]))                        ; 6
fi

What it costs. A value of it is two words, its function and its block; a call passes the block as a hidden first value, and making one allocates the block in r, a bump. A place it keeps is one pointer in the block. A plain task value stays one word and costs nothing more.

Decorators

A startup decorator is a task written above another task with $: $d, or $d(values) for one that takes values. Where the program starts, before main, it runs d(task, values) once, as Python and FastAPI run theirs where a function is defined, so a module can keep what it is given (a web module its routes, say) in a shared field. The task below it stays an ordinary task, which the program calls as usual.

shape Routes
    shared flex count: int = 0
fi

task route(page: (text) -> text, path: text)
    Routes.count++
    show("route ", path, ": ", page("Ada"))
fi

task log(page: (text) -> text)
    show("log ", page("Bob"))
fi

$route("/")
$log
task home(who: text) -> text
    <- "home"
fi

task main()                                       ; before it: log home, route /: home
    show(Routes.count, " route")                  ; 1 route
    show(home("me"))                              ; home: still a task
fi

A decorator takes the task first, of the task's type (see Tasks as values), then the values written after its name, and sends nothing, or nothing ~ E when it can fail. Several go on lines of their own or on one line, $log $route("/"), and the one nearest the task runs first, as in Python. They run after every shared field has its first value, and after the wrappers' values worked out there (below): the modules a program includes first, then each file's tasks in the order they are written. A decorator's values are worked out where the program starts, so they can be literals, macros, shared fields, and calls, but no variable or region, since none exists yet. A decorator may come from a module, $web.route("/"), and may be a shared method, $Registry.enroll (see below). A task takes decorators, and so does a shared method, if it can be a value; main takes no startup decorator, since they run before it.

A decorator that fails stops the program before main: standard error says which decorator failed on which task, and its error, as vela: decorator 'route' failed on task 'home': …; a task of a module is named after the module's name, web.route, however the file includes it. Written $d? or $d(values)?, its failure lets the program go on, to the next decorator and to main, and standard error says so; the task is still a task, but whatever the decorator would have done for it, it didn't. A program built with --no-notices writes nothing for such a failure. A ? on a decorator that never fails is a warning.

task route(page: (text) -> text, path: text) -> nothing ~ text
    when path == ""
        <- fail "an empty path"
    fi
    show("route ", path)
fi

$route("")?
task draft(who: text) -> text
    <- "draft"
fi
; vela: decorator 'route' failed on task 'draft', and the program goes on: an empty path

A wrapper decorator, $(w) or $(w1, w2(values), …), runs at every call of the task instead of once. Calling the task calls the leftmost wrapper, giving it the rest of the chain as a task of the task's type, then its own values, then the task's values; it decides when to call the rest, or whether to, and what it sends is what the call gives. The next wrapper is given the one after it, and the last is given the task itself, as Python's @w1 @w2 wrap a function.

task traced(next: (int) -> int, label: text, n: int) -> int
    show("> ", label, " ", n)
    fixed result = next(n)
    show("< ", label, " ", result)
    <- result
fi

task doubled(next: (int) -> int, n: int) -> int
    <- next(n) * 2
fi

$(traced("sq"), doubled)
task sq(n: int) -> int
    <- n * n
fi

task main()
    show(sq(3))                                   ; > sq 3, < sq 18, then 18
fi

Every call of the task goes through its wrappers: a call by its name, its value (apply(sq, 2)), a startup decorator's, and the task calling itself. A wrapper may send another type than the task does, and the task then sends that to its callers, everywhere it is named, a field's value too; the one before it takes a task sending it. What a public task sends that way is public as its header is, so a private wrapper can't make it send a private type. A wrapper that takes types fits any task its types allow, task noted<R>(next: (int) -> R, n: int) -> R, and one of a region's text, (text@r) -> text@r, passes the region through. A task taking a place or a region takes wrappers too: the wrapper takes the task's values as the task does, flex& n: int or r: region, the rest of the chain is of the task's type, (flex& int) -> int or (@r, int) -> [int]@r, and it gives them on as any call does, next(&n) or next(r, n). What the task sends in its region, the chain sends there too. A wrapper taking types can't find one of them in a region the rest of the chain takes (see places and regions), so a wrapper of a task sending into a region it takes is written for the task's types, task logged(next: (@a, int) -> [int]@a, a: region, n: int) -> [int]@a.

A wrapper's own values are worked out where the program starts, as a startup decorator's are, so they hold no variable and no region, and each is worked out once for every call of the task. One the compiler works out, a literal, a macro, or a task's name, is written into each call and costs nothing; any other, such as a call or a shared field, is worked out once and kept, and each call reads it. They are worked out after every shared field has its first value and before the startup decorators, which may call the task: the modules a program includes first, then each file's tasks in the order they are written.

shape Config
    shared flex depth: int = 2
fi

task label(word: text) -> text
    show("label ", word)
    <- word
fi

task logged(next: (int) -> int, name: text, times: int, n: int) -> int
    show("> ", name, " x", times)
    <- next(n)
fi

$(logged(label("sq"), Config.depth + 1))
task sq(n: int) -> int
    <- n * n
fi

task main()                                       ; before it: label sq
    Config.depth = 10
    show(sq(3))                                   ; > sq x3, then 9: the depth read where the program started
fi
task describe(n: int) -> text
    <- when sq(n) > 4 "big" else "small" fi
fi

task logged(next: (int) -> int, name: text, n: int) -> int
    <- next(n)
fi

$(logged(describe(3)))       ; error: this value of wrapper 'logged' is worked out where the program starts, before task 'sq' can run, and it calls 'sq' through 'describe', …
task sq(n: int) -> int
    <- n * n
fi

task main()
    show(sq(3))
fi

The compiler writes each layer of the chain as a task of its own, and the task's body as the last, so a wrapper costs a call of a task, which the C compiler can inline; nothing is kept for it but the values worked out where the program starts, one variable each. A wrapper written for one list of parameters fits the tasks of that list; one taking a type group fits any, with a region the task takes named beside the group (see places and regions in a group).

Shared methods. A shared method is a task of its shape or kind, called on the type rather than on a value, so it takes decorators as a task does, on the lines above shared m(…) inside its type, and it can be a decorator or a wrapper itself, named on its type: $Registry.enroll, $(Timer.counted), or $m.Registry.enroll from another module. Being one of its shape's methods, such a decorator may change the shape's shared read fields, so a registry keeps its table where only it can change it:

shape Registry
    shared read count: int = 0

    shared enroll(f: (int) -> int, name: text)
        Registry.count = Registry.count + 1
        show("enrolled ", name)
    fi
fi

shape Timer
    shared flex calls: int = 0

    shared counted(next: (int) -> int, n: int) -> int
        Timer.calls = Timer.calls + 1
        <- next(n)
    fi
fi

$Registry.enroll("sq")
$(Timer.counted)
task sq(n: int) -> int
    <- n * n
fi

shape Temperature
    celsius: int

    $Registry.enroll("fahrenheit")
    $(Timer.counted)
    shared fahrenheit(celsius: int) -> int
        <- celsius * 9 / 5 + 32
    fi
fi

task main()                                       ; before it: enrolled sq, enrolled fahrenheit
    show(sq(3), " ", Temperature.fahrenheit(100))  ; 9 212
    show(Registry.count, " ", Timer.calls)        ; 2 2
fi

Wrappers on methods of every value. $( … ) goes above a method of a shape or a kind too, and runs at each call, a.withdraw(3) or .withdraw(3) inside another method. A method is a task taking the value it is called on first, so a wrapper is given that value first: as a place, flex& a: Account@s, where the method alters it, and the rest of the chain takes it so too. A wrapper taking a group fits any method; a region the method takes is never in the group but named beside it, after the value, as in next: (Pad, @a, P...) -> R, p: Pad, r: region, args: P.... A value of a type holding text or pointers may be in any region, and so may the value a method is called on, so a wrapper written for one method names that region, Account@s, as the message for a missing one says.

A method of the same type wraps another as $(.checked): it is called on the same value, so it reads its private fields and calls its other methods, and it takes the rest of the chain first, a task taking the value first, which it gives on: next(&.this, amount). A method wrapping one that alters the value alters it too, and only such a one does: one that only read the value would see it change under it, and one that altered it would change a value the wrapped method may be called on as one that never changes. Its own values, $(.limited(10)), are worked out where the program starts, as any wrapper's are, so they read none of the method's values or fields.

task audited<P...: shown, R>(next: (P...) -> R, args: P...) -> R
    show("audit: ", args.len, " values")
    <- next(args...)
fi

shape Account
    owner: text
    read balance: int

    alters checked(next: (flex& Account@a, int) -> bool, amount: int) -> bool
        when amount > .balance
            show("refused ", amount, " for ", .owner)
            <- false
        fi
        <- next(&.this, amount)
    fi

    $(audited)
    alters deposit(amount: int)
        .balance += amount
    fi

    $(.checked)
    alters withdraw(amount: int) -> bool
        .balance -= amount
        <- true
    fi
fi

task main()
    flex a = Account { owner: "Ada", balance: 0 }
    a.deposit(5)                    ; audit: 2 values
    show(a.withdraw(3))             ; true
    show(a.withdraw(9))             ; refused 9 for Ada, then false
fi

A plain task wrapping a method reads only what any task reads of the value: no private field of another file, and no read field it could change. A method takes no startup decorator, and a method of a shape that takes types takes no wrappers yet, as a task taking types doesn't (see Future Directions).

Type groups

P... among the types a task takes stands for a group of types: any number of them, of any mix. args: P... takes the values a call gives between the ones the task names before it and after it, usually all of them, as its last parameter; (P...) -> R is a task taking those values; and f(args...) gives them on, whole, where the group goes. So one task fits any task's values, which is what a wrapper needs:

task call<P..., R>(f: (P...) -> R, args: P...) -> R
    <- f(args...)
fi

task traced<P..., R: shown>(next: (P...) -> R, label: text, args: P...) -> R
    show("> ", label)
    fixed result = next(args...)
    show("< ", label, " ", result)
    <- result
fi

task add(a: int, b: int) -> int
    <- a + b
fi

$(traced("area"))
task area(w: int, h: int) -> int
    <- w * h
fi

$(traced("first"))
task first(t: text@r) -> text@r
    <- t[0..1]
fi

task main()
    show(call(add, 2, 3))                         ; 5
    show(area(3, 4))                              ; > area, < area 12, then 12
    show(first("Ada"))                            ; > first, < first A, then A
fi

Each use fills the group in with the types of the values it gives, as a use fills in a type a task takes: call(add, 2, 3) makes P the group int, int, and the task made for it takes two ints, so a group costs nothing when the program runs. A group may hold no types (call(seven)), large values, and text of a region, whose region goes through.

A group's values are given on whole, f(args...), so every value reaches a parameter of its own type, or counted, args.len, a number each use knows; an index or a sum it is part of is checked as the program runs, as any value's is. They go to a task value whose type takes the group, or to a named task taking a group, between the values it names: log("calling", args...). Values written just before or after them join that task's group, log("x", 1, args...), as many as the task doesn't name after its group, except in a call of the task by itself, whose group would grow at each call; a task calling itself gives its group on whole. A task taking a set number of values is never given a group's, whose number is known only where the group is filled in.

task log<P...>(label: text, args: P...)
    show(label)
fi

task traced<P..., R>(next: (P...) -> R, args: P...) -> R
    log("calling", args...)              ; the group goes on to log's group
    <- next(args...)
fi

A group can ask something of each of its types, as a type a task takes can, written after its dots: P...: shown, or several joined with &. Each value a use gives must allow it, and a group given on to one that asks something must ask it too. A group of shown types is written by show(args...), every value one after another, as show(a, b) writes them:

task log<P...: shown>(label: text, args: P...)
    show(label, " (", args.len, "): ", args...)
fi

task main()
    log("values", 1, " and ", 2.5)                ; values (3): 1 and 2.5
    log("nothing")                               ; nothing (0): 
fi

each v in args gives each value in turn, of its own type: the body is written once for each value, and in it P alone is that value's type (fixed kept: P = v, Box<P>, new [P]() at r), written nowhere else. A group of none runs it never. A loop over the same group inside it is an error, since P would name two types at once: give the value and the group on to a task of its own, with(v, args...), which goes through them.

(args...), or args.tuple, makes the values a tuple, whose items never change, so the group asks its types to be frozen. A tuple holds two or more items, so its type is (P...) ~ maybe P#0: the tuple for two or more values, and for one or none a failure holding the one value, or none, which a plan B, match, or is handles. Its items are t.0 and t.1, which every tuple has, and one past them, t.2, is a mistake of any use giving fewer values, unless a when on args.len leaves it out there: each use knows args.len, so it keeps only the branch it takes, a when used as a value too, and leaves out what follows a part it knows is false, as t.2 in args.len > 2 and t.2 > 0. t.#2 reads it in a maybe instead. Since each use gives its own number of items, the tuple can't be unpacked into names. Messages call the type of item k P#k.

task described<P...: frozen & shown>(args: P...)
    fixed t = (args...) {
        failure one
        when one is {v}
            show("one value: ", v)
        else
            show("no values")
        fi
        <-
    }
    show("first two: ", t.0, ", ", t.1, "; a third: ", t.#2)
    when args.len > 2
        show("the third: ", t.2)
    fi
fi

task main()
    described(1, "two", 3.5)      ; first two: 1, two; a third: { 3.5 }, then the third: 3.5
    described(7)                  ; one value: 7
    described()                   ; no values
fi

Places and regions. A task names values one by one before and after its group, and a task's type does too: task framed<P...>(open: text, args: P..., close: text) is called framed("(", 1, 2, ")"), and (@a, P...) -> R takes a region, then the group. A group holds the values of a task taking places: a wrapper reads them as copies, through show(args...), (args...), each and t.#k, and gives them on as places with next(args...), so the task still changes the caller's variables. Nothing writes through a group's value, and the call that gives the places checks them, as any call does: each marked, one that changes given once. A call gives a group a place only where it also gives a task value taking one there, use(put, &k), since only that says how it is taken.

A group never holds a region: a region is always named, beside the group, so a wrapper of a task taking one names it as the task's type does, and can send in it. The values a wrapper names around its group are the task's, as many as its next task's type names there; its own values come before them.

task traced<P...: shown, T>(next: (@a, P...) -> [T]@a, r: region, args: P...) -> [T]@r
    show("calling with ", args...)
    <- next(r, args...)
fi

task counted<P..., R>(next: (P...) -> R, args: P...) -> R
    show("values: ", args.len)
    <- next(args...)
fi

$(traced)
task squares(r: region, n: int) -> [int]@r
    flex made = new [int]() at r
    each i in 1..n + 1
        made.add(i * i)
    fi
    <- made
fi

$(counted)
task put(flex& n: int)
    n = n * 10
fi

task main()
    region kept
    show(squares(kept, 3))          ; calling with 3, then [1, 4, 9]
    flex k = 3
    put(&k)                         ; values: 1
    show(k)                         ; 30
fi

A task takes one group, anywhere among its parameters, and a task's type one group, with the values it takes alone around it. The group itself is never marked (flex& args: P..., (flex& P...) -> R): its values are taken as the task given for it takes them. Only a task takes one, not a shape, and a call doesn't write a group's types yet.

Names

A name starts with an ASCII letter and continues with ASCII letters, digits, and _, but cannot contain __ (two underscores), which the compiler keeps for its own C names. Words that mean something in C, such as int, default, or stdout, are ordinary names in Vela.

Tasks, shapes, kinds, variants, and macros share one namespace in a file: no two of them can have the same name, even across different kinds, and none can be named like a builtin: the types (int, float, bool, unit, char, text, nothing), the tasks (show, scan), or the kinds no file declares and their variants (ParseError with NotANumber and TooLarge, InputError with Ended and NoNumber). Another module has names of its own (see Modules). Every type you write must be a builtin, a shape, a kind, or a task's type. A variable can't take the name of a variant, a macro, a kind, or a module the file includes, so a bare Red always means the variant, P the macro, and Light.Red a variant of Light. A variable that holds no task may be named like a task, and then its name means the variable, while name(…) still calls the task; one that holds a task may not.

Control Flow

when x > 0
    show("positive")
else
    show("non-positive")
fi

loop count < 10
    show(count)
    count = count + 1
fi

each i in 0..5
    show(i)
fi

each counts from the start of the range up to, but not including, the end. Both ends are evaluated once, before the first iteration; the loop variable is immutable. A range such as 0..5 is not a value: it can only appear in each and in a slice. each also goes through the items of an array or slice, as in each score in scores, or through a place as it is at each pass, each score in &scores (see Arrays).

loop repeats while its condition holds. The condition follows loop directly: loop when count < 10 says a 'loop' takes its condition directly, without 'when'. A loop also stops early with break, ends a pass early with continue, or leaves the task with <- (see Stopping a loop: break and continue). A loop may also start and step names of its own, loop flex i = 0 || i < 10 || i++, and one that never ends by itself is written endless (see A loop in three parts). A loop can be a value too, its final pass's last line, fixed r = each x in xs … fi (see Loops that give a value).

A when or a match can be a value as well as a statement, giving one from each branch (see A match or a when as a value).

block starts a block of its own, closed by fi, which runs its statements once, where it is. What it declares ends at its fi: its names, which another block may use again, and its regions, which are freed there with the files opened in them, though the task goes on (see A region lives until its block closes):

flex total = 0
block
    region r
    flex f = File { path: "notes.txt" }
    f.open(FileMode.Read) at r { failure e
        <-
    }
    total = f.size() { 0 }
fi                           ; the file is closed here, and r freed
show(total)

A block takes nothing on its line. What it sets is set after it, a result (total <- …) too, and a block every path of which sends counts as sending. It is a statement, not a value.

else when chains conditions: each is tried in turn, the first that holds runs its block, and one fi closes the whole chain. It does what a when written inside the else on its own lines does, but stays one statement however many arms it has, with no limit on them. It may end with an else, or with nothing when no condition need hold. else if is an error that says so.

task grade(score: int) -> text
    when score >= 90
        <- "top"
    else when score >= 60
        <- "pass"
    else
        <- "fail"
    fi
fi

task main()
    show(grade(72))       ; pass
fi

A condition can also test a case form with is, the same forms a case takes ({v}, none, a variant, a tuple taken apart, failure e). The names it binds are visible only in the block it guards (not in the else). A name is a fixed unless the case form writes flex before it, and parentheses take a tuple apart (see Tuples).

An is can be joined with other parts by and, before or after it, in a when, an else when, a loop, and a when used as a value. The names a case form binds are seen by the parts after its and and by the block, and each part is tested only when the ones before it held; the else runs when any of them didn't. An is is never joined with or, nor written after not: its names would then be bound on one path only. An or in parentheses is one part: m is {x} and (x == 1 or x == 2).

fixed b = scan("count:", int) { none }
when b is { inside } and inside < 0
    show("Invalid count")
fi

when ready and half(n) is {h} and half(h) is {q} and h > q
    show(h, " ", q)
fi

loop half(i) is {h} and h > 1    ; while i halves to more than 1
    i = h
fi

when b is {x} or x > 0             ; error: an 'is' is joined with 'and', not 'or'
when light is Dimmed level
    show(level)
fi

loop current is {flex node} ; while current holds a node
    show(node^.value)
    current = node^.next
fi
fixed found: maybe (text, int) = ("Ada", 36)
when found is {(name, age)}       ; the value, then its items
    show(name)
    show(age)
fi
fixed pair = (1, "one")
when pair is (number, word)       ; always matches: a tuple has no other case
    show(number)
    show(word)
fi
when pair is flex (n, w)          ; flex: the names can be assigned
    n = n + 1
    show(n)
    show(w)
fi

A loop in three parts, and endless

A loop may have three parts, parted by ||: a start, which runs once before the loop; a condition, tested before each pass as a plain loop's is; and a step, which runs after each pass. The start and the step may each hold several items, parted by ,, and any part may be empty. endless is a loop that never ends by itself, the same as loop || || said in a word. A name the start declares is flex, fixed or bind; a bind one changes only its contents, loop bind b = Box { value: 1 } || b.value < 5 || b.value++, so loop bind i = 0 || i < 3 || i++ is an error.

task main()
    ; A name of its own, which lives in the loop, and several, side by side.
    loop flex i = 0 || i < 3 || i++
        show(i)                      ; 0, 1, 2
    fi
    loop flex lo = 1, hi = 9, fixed gap = 4 || hi - lo > gap || lo++, hi--
        show(lo, "-", hi)            ; 1-9, 2-8
    fi

    ; Names already declared come first, and keep their last value.
    flex n = 0
    flex steps = 0
    loop n = 27, steps = 0 || n != 1 || steps++
        n = when n % 2 == 0 n / 2 else 3 * n + 1 fi
    fi
    show(steps)                      ; 111

    ; Each part may be empty; with no step, the last '||' may go too.
    flex k = 0
    loop || k < 3 || k++
        when k == 1
            continue                 ; the step still runs
        fi
        show(k)                      ; 0, 2
    fi
    endless
        k++
        when k > 5
            break                    ; the step would not run
        fi
    fi
    show(k)                          ; 6
fi

Stopping a loop: break and continue

break stops the loop around it there, and the code after the loop goes on. continue ends this pass of the loop, which goes on with its next: a loop tests its condition again, and an each takes its next item. Either may stand anywhere in the loop's body, inside a when, a match, a block, or a plan B, and is a statement of its own, which nothing follows on its line.

task main()
    fixed scores = [7, -1, 12, 0, 30]
    flex total = 0
    each s in scores
        when s < 0
            continue                 ; skips this score
        fi
        when s == 0
            break                    ; stops at the first 0
        fi
        total = total + s
    fi
    show(total)                      ; 19

    ; A name before a loop's word lets a loop inside it stop it.
    outer => each row in 0..3
        each col in 0..3
            when col > row
                continue outer       ; on with the next row
            fi
            when row == 2
                break outer          ; stops both loops
            fi
            show(row, col)           ; 00, 10, 11
        fi
    fi

    ; A plan B may stop the loop instead of giving a value.
    fixed words = ["one", "two", "three"]
    flex i = 0
    endless
        show(words[#i] { break })    ; one, two, three, then it stops
        i++
    fi
fi

Loops that give a value

A loop or an each can be a value, wherever a value goes: fixed r = loop … finally v fi, an argument, a condition, what a task sends. In single-value form, a finally block before fi runs once after the loop finishes all iterations normally and gives the summary value. In map form, a loop without finally whose passes end with a value collects each pass's value into a collection (an array [T#N] for constant counts, or a slice [T]@r for dynamic counts). A loop may also end without a value, and then it fails with LoopResult<B, M>, a kind of Vela's library, std/loops: so its value is a V ~ LoopResult<B, M>, read with a plan B, a match, or is.

include std/loops {LoopResult}

task main()
    fixed xs = [3, 8, 1, 9, 4]
    ; The finally block gives the summary value.
    flex last = 0
    fixed doubled = each x in xs
        last = x * 2
    finally
        last
    fi
    show(doubled { 0 })                   ; 8

    ; A break stops it with a value, which it fails with.
    flex last_big = 0
    fixed big = each x in xs
        when x > 8
            break = x
        fi
        last_big = x
    finally
        last_big
    fi
    match big
    case {v}
        show("ended with ", v)
    case failure why
        match why
        case Broke b
            show("broke at ", b { 0 })      ; broke at 9
        case Skipped
            show("no items")
        case EndedByContinue
            show("the last pass continued")
        case Manual m
            show("the last line failed: ", m)
        fi
    fi

    ; The finally block may fail on purpose, and LoopResult written bare
    ; on the loop's own variable takes the loop's types.
    flex last_checked = 0
    fixed checked: int ~ LoopResult = loop flex i = 0 || i < xs.len || i++
        last_checked = xs[i]
    finally
        when last_checked == 4 fail LoopResult.Manual(last_checked) else last_checked fi
    fi
    show(checked { -1 })                   ; -1: the final pass failed

    ; The variable names the loop, for a break from a loop inside it.
    fixed pair = each a in 1..4
        each b in 1..4
            when a * b == 6
                break pair = a * 10 + b
            fi
        fi
    finally
        0
    fi
    show(pair { -1 })                      ; -1: a break stopped it

    ; As a statement with a plan B, it gives nothing, and only fails.
    each x in xs
        when x == 1
            break
        fi
    fi {
        show("found a one")              ; found a one
    }
fi

A match or a when as a value

A match or a when can be the value itself, wherever a value goes: the right side of a fixed, an argument, part of a larger expression, or what a task sends. Every branch then gives the value. A case gives it on one line with =, or on the last line of its block, as a plan B's block does; a when uses blocks only, and needs its else, since without one nothing gives the value when the condition doesn't hold.

; The same `area` as in Kinds, with the match as the value it sends.
kind Shape
    Circle(radius: float)
    Rectangle(width: float, height: float)
fi

task area(s: Shape) -> float
    <- match s
    case Circle r = 3.14 * r * r
    case Rectangle w h = w * h
    fi
fi

; Shapes come from here, so `main` reads them as any program would.
task made(wide: bool) -> Shape
    <- when wide
        Circle(2.0)
    else
        Rectangle(1.0, 2.0)
    fi
fi

task main()
    fixed s = made(true)
    show(area(s))                      ; 12.56

    ; A case may work first, and give the value on its last line.
    fixed name = match s
    case Circle r
        fixed wide = r > 1.0
        when wide
            "a wide circle"
        else
            "a circle"
        fi
    case Rectangle w h = "a rectangle"
    fi
    show(name)                         ; a wide circle

    ; A `when` used as a value, with a chain of conditions.
    fixed size = when area(s) > 100.0
        "huge"
    else when area(s) > 1.0
        "middling"
    else
        "tiny"
    fi
    show(size)                         ; middling
fi

The rules are the ones the pieces already have:

Maybe and Errors

A value that might be missing has type maybe T. Wherever a maybe is expected (a typed binding, an assignment, an argument, a field, or a send), a plain value becomes one, and none is the empty one. A maybe can't be used as its value directly: m + 1 is an error, and == compares it only with none. What it holds is compared with ~=: m ~= 4 is true when m holds 4, and false when it holds another number or is empty (see Comparing). A maybe can't hold another maybe.

task half(n: int) -> maybe int
    when n % 2 == 0
        <- n / 2               ; the int becomes a maybe int
    fi
    <- none
fi

fixed m = half(7)
show(m)                    ; none
show(m == none)            ; true
show(half(8) ~= 4, " ", m ~= 4) ; true false
show(half(8) == 4)          ; error: '==' compares a maybe int only with none; ... or compare what it holds with '~='
show(m + 1)                ; error: arithmetic requires numeric operands; the value in this maybe int must be extracted first

== refuses a maybe beside a plain value so that an empty one is never mistaken for a value by accident. To compare on purpose, say what an empty one means: ~= is false when it is empty; a plan B gives the value to compare instead, so pick one that can't be mistaken for a real one; and a when, as a value or joined with and, names the value first.

fixed m = half(8)
show(m ~= 4)                                     ; true, and false when m is empty
show(m { 0 } == 4)                                ; true: an empty m counts as 0
fixed same = when m is { x } x == 4 else false fi   ; true
when m is { x } and x == 4                      ; see is joined with and
    show("four")
fi

There are three ways to get the value out. match handles both outcomes:

match half(8)
case {n}
    show(n)                ; 4
case none
    show("odd")
fi

is runs a block only when there is a value (see Control Flow): when half(8) is {n}. And a plan B, a block after the value, gives the value to use when there is none:

fixed a = half(7) { 0 }            ; 0
fixed b = half(7) { 1 } * 10       ; 10: a plan B works inside expressions
fixed c = half(7) {
    show("odd, using 3")
    3                              ; the last line is the value
}
fixed d = half(7) {
    <-                             ; or leave the task instead
}

The value before the block is evaluated once, and the block runs only when it is empty. Its last line gives the value, of the type inside the maybe (or none, below), and may be a when or a match, which gives it through their branches (see A match or a when as a value); a <- in the block sends from the task as usual, and when every path through the block sends, it needs no value line. Names declared in the block stay in it, and an empty block is an error. A call with a plan B can be a statement of its own: half(7) { <- } leaves the task when there is no value, and drops the value otherwise. A { after a shape's name followed, on the same line, by field: or } is a shape literal; any other { after a value starts a plan B, and a <- may follow it on the same line: m { <- 0 }.

A plan B may give none instead, and the whole is then a maybe: the value when there is one, and empty when the plan B runs. It is a maybe with or without a written type, and never a plain value, which would have nothing to hold when the plan B runs. So a value that can fail becomes a maybe when its error doesn't matter, and on a value that is a maybe already, m { none } is m itself.

fixed b: maybe int = scan("enter:", int) { none }  ; a number, or none when the line isn't one
fixed c = scan("enter:", int) { none }              ; maybe int too
when b ~= 10
    show("ok")
fi
fixed n: int = scan("enter:", int) { none }         ; error: this plan B can give none, so the whole is maybe int
show(half(8) { none } == 4)                    ; error: '==' compares a maybe int only with none

A task that can fail sends T ~ E: its value when it works, or an error of type E, sent with <- fail error. The error type must differ from the value type, and can be any type but nothing, a maybe, or another T ~ E: a type has one error side, so (int ~ text) ~ text is refused when written, as maybe maybe int is (a generic may still make either, below). Two words keep the two jobs apart: fail is the verb that makes a failure, and failure is the noun that names one where it is matched. So case {v} matches a success, case failure e a failure, and a plan B's first line failure e names the failure it handles — the name of the error value, not a type; the type is the one after ~:

kind SizeError
    Negative(value: int)
    TooBig
fi

task checked(size: int) -> int ~ SizeError
    when size < 0
        <- fail Negative(size)
    fi
    when size > 100
        <- fail TooBig
    fi
    <- size
fi

task main()
    fixed a = checked(500) { 100 }       ; ignore the error: use 100
    fixed b = checked(-1) {
        failure e
        match e
        case Negative value
            show(value)            ; -1
        case TooBig
            show("too big")
        fi
        0
    }
    match checked(7)
    case {size}
        show(size)                 ; 7
    case failure e
        show("failed")
    fi
fi

nothing ~ E is a task that sends nothing when it works: a bare <-, or reaching the end, succeeds, and case {_} matches that success. A call written as a statement can't drop an error by accident: when its ~ result goes unused, that is an error. (A result kept with fixed stays a T ~ E that only a match or a plan B opens, but nothing yet checks that such a variable is ever used.) An empty plan B, save(file) { }, ignores the error on purpose. fail binds like - and not, so an error computed with operators needs parentheses.

task save(size: int) -> nothing ~ text
    when size > 100
        <- fail "too big"
    fi
    show("saved")
fi

save(500)                  ; error: the error 'save' may send is ignored
save(500) {
    failure reason
    show(reason)           ; too big
}

Becoming a wider type

A plain value becomes a maybe or a T ~ E by being wrapped: the value is kept as it is, and the emptiness, or the error side, is added around it. That is all of it, and it has one consequence worth knowing: a value already made is never changed inside. A value written out is built part by part, so each part becomes what that part's type is; a value that comes back from a task is already made, and nothing reaches into it.

fixed pair: (int, maybe int) = (2, 3)      ; written out, so built item by item
fixed made = divide(17, 5)                 ; (int, int), already made
fixed taken: (int, maybe int) = (made.0, made.1)  ; its items, built into the type wanted
fixed same: (int, maybe int) = made
; error: expected type (int, maybe int), got (int, int); a value already made is never changed inside, …

Where several values must have one type — the items of an array, the branches of a match or a when used as a value — the same rule decides what that type is: the types are joined, and a wrapper goes outside whatever is already wrapped.

fixed m: maybe int = 3
fixed xs = [1, none]                ; [maybe int#2]
fixed ys = [m, fail "no"]           ; [maybe int ~ text#2]: the maybe kept, an error side added
fixed zs = [risky(1), none]         ; risky sends int ~ text: [maybe (int ~ text)#2]
fixed ws = [risky(1), m]            ; error: an array's values have one type, but this one is maybe int …

The middle two are the two nestings, and which one comes out follows from what was wrapped already: a maybe int beside a failure keeps its maybe and gains an error side, and an int ~ text beside none keeps its error side and goes inside a maybe. Two values that would each need the other remade, like int ~ text beside maybe int, have no type in common: which one is meant is the program's to say.

Writing the two apart. ~ holds the whole type before it, so maybe int ~ text means (maybe int) ~ text: a text failure, or a success holding maybe an int. Parentheses group a type, so the other nesting is maybe (int ~ text): nothing, or a result that may have failed. One type in parentheses is only that type, and a tuple still needs two or more items. A maybe and an error side nest either way round, but neither nests inside itself when written: maybe maybe int and (int ~ text) ~ text are both refused. A generic can still make one, since the type it is given for T may be a maybe or a T ~ E itself: wrap<T>(x: T) -> maybe T given a maybe int sends a maybe maybe int, whose outer none means the value is missing and whose inner one means it was there and held nothing (it shows as { none }). ~= compares it with the maybe int it holds, a plan B on it gives that maybe int, and a plan B giving none in a generic, as in x { none } with x: T ~ text, gives a maybe T whatever T is.

Call pipes

A pipe gives a value to a call written after it, so a chain of calls reads in the order it runs: a |> f(b) is f(a, b). Each further > gives the value to the next argument instead: a |>> f(b) is f(b, a), and a |>>> f(b, c) is f(b, c, a). A line that starts with |> goes on with the chain of the line before it, so a long chain is written one step per line, with comments and blank lines between steps if wanted. std/collections has tasks written for pipes, taking the slice first and a one-line lambda after it: nums |> filter(n -> n > 2) at r |> map(n -> n * 10) at r.

A step that can fail, giving T ~ E, gives the next step its T: the next step runs only when it succeeded. The first failure stops the chain, and the chain fails with it; when every step succeeds, the chain's value is the last step's. So a chain with a step that can fail is a T ~ E itself, read as any other is, and a plan B after a step handles that step and every one before it: one plan B at the end handles the whole chain. Steps that cannot fail just pass their value on, and a chain with none that can fail is a plain value.

kind ReadError
    Empty
    NoCount
fi

kind StockError
    SoldOut(left: int)
fi

kind OrderError::ReadError & StockError
fi

task quantity(line: text@r) -> int ~ ReadError
    when line.len == 0
        <- fail Empty
    fi
    <- int(line) { <- fail NoCount }
fi

task reserve(wanted: int, left: int) -> int ~ StockError
    when wanted > left
        <- fail SoldOut(left)
    fi
    <- wanted
fi

task price(each_cents: int, count: int) -> int
    <- count * each_cents
fi

task double(n: int) -> int
    <- n * 2
fi

task total(line: text@r) -> int ~ OrderError
    <- line
        |> quantity()           ; int ~ ReadError
        |> reserve(5)           ; reserve(count, 5): int ~ StockError
        |>> price(250)          ; price(250, count): cannot fail
fi

task main()
    show(3 |> double() |> double())        ; 12
    each line in ["2", "", "x", "9"]
        fixed cents = line |> total() { failure e
            show("refused: ", e)
            0
        }
        show(cents)     ; 500; refused: Empty, 0; refused: NoCount, 0; refused: SoldOut(5), 0
    fi
fi

The steps of a chain may fail with different kinds, and a chain fails with one. They join as the branches of a when do: the same kind, or the one that imitates the others; or, as here, the kind the place the chain goes names, where it imitates every one of them: the type of a binding (fixed n: int ~ OrderError = line |> quantity() |> reserve(5)) or the task's own when the chain is sent with <-. Each failure becomes a value of that kind where it happens. Steps whose kinds join in none of these ways are an error, which says to declare a kind imitating them.

After |> comes one call, written with its parentheses: a task, one of a module (|> steps.read()), a method of another value (|> till.plus()), a shared method, a variant, a task held in a variable, or .m() inside a method. Its at r stays its own: n |> label() at r is label(n, r). The value is worked out first, then the call's own arguments, left to right, and it fits its parameter as any argument does (it may become a maybe, or a value of a kind imitating its own). A pipe binds below + and above comparisons, so a + 1 |> f() == 3 is f(a + 1) == 3; the call is the whole of its step, so a |> f() + 1 is an error, written (a |> f()) + 1.

Every mistake in a pipe is found while compiling: a position past the arguments written plus one, an argument of the wrong type, the wrong count, a step that gives several results or nothing to the step after it, a flex&, bind& or fixed& parameter (a pipe gives a value, never a place), a call on what another call gives (x |> make().plus(), which reads as if make were given the value), a lambda written after |>, and a chain whose failure is dropped on a line of its own. A pipe carries failures but not absence: a step giving maybe T to one that takes a T is an error, and a plan B in that step reads it (… |> find() { 0 } |> g()).

Lazy iterators

std/collections also holds Iterator<T>, a pattern for reading one item at a time: an iterator answers next() with the next item or none. In a file that includes the module, a slice has its chain members, and so does every iterator: the makers map, filter, take, skip, zip, and enumerate, and the consumers collect, fold, find, all, and any. Each step reads the one before it: nums.filter(n -> n % 2 == 0).map(n -> n * 10).collect(r) gathers nothing until collect asks, so a large collection is never held whole. Otherwise a slice has no methods; the error says to include the module.

The makers give iterators of their own shapes, which follow Iterator; zip gives a Pair (fields .first and .second) for each place of the two, as long as the shorter, and enumerate an Indexed (fields .index and .item) from 0. Either side of zip may be a slice, read as its own iterator. collect(r) gathers what is left into a slice in r; fold, find, all, and any read what is left and give one value. Each takes a plain lambda or one that keeps what it uses, xs.map([@r] n -> n * k); a maker holds it in a field (see In a field), so the iterator it makes lives in that lambda's region.

Reading an item advances the iterator, so a consumer is alters and asks for a changeable place or a value made where it is written: flex it = nums.take(3), then it.collect(r), or a chain used where it is written. each x in it goes through an iterator the same way, one next() per pass, so it is flex (or a written expression), and after the loop it is done: it.next() gives none. An iterator is a value like any other: kept in a variable, passed to a task, and copied like its shape. An each over an iterator is a pass loop: it gives no value and takes no finally (its end is a break); gather what it gives with collect.

Operators a pattern names

A shape or kind following a pattern whose methods are named plus, minus, times, div, rem, neg, or cmp — or defining them directly as shape methods — answers the operator each names: a + b is read as a.plus(b), a * b as a.times(b), -a as a.neg(), and a comparison a < b, a <= b, a > b, a >= b as a.cmp(b) read beside 0. Both operands are the one type; beside a value of another type the operation is refused, and the message names the method to call instead. The pattern's header says what the method takes and gives — std/int512's Numeric<T> asks for plus(other: T) -> T and the rest, and Int512 follows it — so Int512 writes arithmetic naturally, and a task taking <N: Numeric<N>> does a + b on its values. == and ~= keep their own meanings for shapes, and ** and the bitwise operators are not answered this way. What an operator does is the method's own: Int512's trap on overflow and on division by zero, for instance, is kept by + and /.

Fixed arrays and slices answer built-in collection operators: [T#N] + [T#M] joins arrays on the stack into [T#(N+M)], and arr * K repeats an array K times at compile time into [T#(N*K)]. Slices join with xs + ys at r (or xs + ys when a default region is in scope) to produce a new slice in the destination region. Adding an item with xs + item appends, item + xs prepends, and in-place joins are written as xs += ys or xs += item.

The type of a value

Every value has x.type: its type as code writes it, as text. The compiler knows it, so it costs nothing to run, and the value itself is never worked out, as C's sizeof doesn't: f().type never calls f. Regions are left out, since a region's name means nothing where the text is read ([float] for a slice [float]@r, ^Point for a ^Point@r), a shape or kind is its own name wherever it is read (Crate, not shapes.Crate), and a shape that takes types is written with them, Box<int>. In a task that takes types it is the type each use gives, so v.type in named<T>(v: T) is int for named(1). Inside a method, .type is the type of the value the method is called on. type is a reserved word, so no field, method, or variable takes the name; x.type = … and x.type() are errors that say what it is.

shape Point
    x: int
    y: int
fi

task named<T>(v: T) -> text
    <- v.type
fi

task main()
    region r
    fixed xs = [1, 2, 3]
    fixed items = new [float]() at r
    fixed p = new Point(Point { x: 1, y: 2 }) at r
    show(xs.type, " ", items.type, " ", p.type, " ", p^.type)   ; [int#3] [float] ^Point Point
    show((1, "a").type, " ", named(xs))                   ; (int, text) [int#3]
    fixed m: maybe int = none
    show(m.type)                                         ; maybe int
fi

Memory and Regions

Vela has no garbage collector and no free. Values are copied and live where they are declared; a value that needs to be shared or linked, like the nodes of a list, is allocated with new in a region: an arena that gives out memory until the block declaring it closes, and then frees everything in it at once. Every pointer's type names its region, and the compiler makes sure no pointer is ever stored where it could outlive that region. So a Vela program can't use freed memory, free anything twice, or leak memory past the block that owns it, and the compiler does this with a handful of rules rather than a borrow checker.

This section is the reference. Vela Regions is the longer guide: why Vela has regions, their pros and cons, which types need a region and which don't, a step-by-step tutorial, the current limitations with a workaround for each, and ideas for where regions could go next.

The rules in short.
  • region r declares a region; region r1, r2, ... declares multiple regions on one line; new T(v) at r (or new@r T(v)) copies a value into it.
  • A pointer into r has type ^T@r, and a type can only name regions visible where it is written.
  • When the block declaring r closes, however it closes, everything in r is freed.
  • A task allocates only in a region it is given as a parameter, r: region; it can read and link pointers in any region its parameters' types name.
  • region r default (and r: region default) makes r the block's default region: what allocates there needs no at r.
  • := on variable definitions allocates directly into the block's default region (e.g. bind x: ^int := 4, flex s := [1, 2, 3]).
  • A pointer is never stored in a different region than the one its type names.

Allocating in a region

shape Point
    x: int
    y: int
fi

task main()
    region scratch                          ; a new, empty region
    flex n = new int(42) at scratch         ; n is a ^int@scratch
    flex p = new@scratch Point(Point { x: 1, y: 2 })
    n^ = n^ + 1                             ; read and write through ^
    p^.x = 5
    show(n^ + p^.x)                         ; 48
fi                                          ; scratch, and all in it, is freed here

new T(value) at r and new@r T(value) are the same: both copy the value into region r and give back a ^T@r. The type after new names no region, since what it makes lives in the region it allocates in. A pointer to a shape has no fields of its own: read and write them through ^, as in p^.x (p.x is an error that says so). What a pointer points to is written through a flex name: nothing changes through a fixed one, at any depth, so fixed p = new Point(…) at r then p^.x = 5 is an error, which says cannot change 'p^.x': nothing changes through immutable variable 'p', at any depth.

The empty value needs no ( ). Where a new gives no value, the parentheses may be left out: new [text] at r is the same as new [text]() at r, an empty slice, and new [:float] and new [text: int] are empty maps. A shape whose fields all fall back on values (see fields with defaults) is made the same way: new Point at r and new Point() at r both copy Point {}, the value of its defaults, into r. Nothing else has an empty value, so new int at r, a kind, or a shape with a field that falls back on none is an error that names what is missing. Both forms make the same program; the one with ( ) stays. A { after the type is an error too: the value goes in ( ), and a new never fails, so no plan B follows it.

region r default
flex names = new [text]             ; [text]@r, as new [text]()
flex prices = new [:float]          ; [text: float]@r
fixed origin = new Point            ; ^Point@r, as new Point(Point {})

Pointer types name their region

A pointer type is written ^T@r. The @r comes after ^ and maybe and before ~, so maybe ^Node@r is a maybe pointer into r, and ^Node@r ~ text is such a pointer or a text error. A type without pointers lives in no region: int@r and Point@r are errors. A pointer always points to something: one that may be missing is a maybe ^T@r, which must be checked before it is used (see Maybe and Errors), and costs nothing extra, since the C it becomes is a plain pointer.

region r
flex next: maybe ^int@r = none
when next == none
    next = new int(1) at r
fi
when next is {p}
    show(p^)                               ; 1
fi
show(next^)                                ; error: this pointer is a maybe ^int@r, which may be none
fixed bad: ^int@r = none                    ; error: expected type ^int@r, got none; a pointer can't be none, so use 'maybe ^int@r'
fixed lost: ^int = new int(2) at r         ; error: a pointer lives in a region, so its type names one

A region lives until its block closes

A region can be used from its region statement to the end of the block that declares it, like a variable, and shares the variables' names (no shadowing). Since a type can only name a region visible where the type is written, no variable, field, or result outside the block can hold a pointer into the region, so nothing is left pointing into it when it is freed. And a pointer is never stored in another region, which might live longer:

flex saved: maybe ^int@inner = none     ; error: unknown region 'inner'; declare it with 'region inner'

region outer
flex p = new int(0) at outer
when ready
    region inner
    p = new int(1) at inner                ; error: expected type ^int@outer, got ^int@inner;
fi                                         ;   a value can't move to another region

The region is freed however the block ends: at its fi, at the end of a plan B's block (after its last line has given its value), or by a send (<-) from anywhere inside it, which frees every region the task has declared before it returns (after computing the value it sends). A region in a loop's body is a new region on every pass, freed at the end of the pass, which is how a loop allocates without growing, and a region in a block is freed at the block's fi, without a loop or a condition around it:

each line in 0..1000
    region work                            ; empty again on every pass
    fixed temporary = new int(line) at work
    show(temporary^)
fi

There is no way to free one value early: free p is an error that explains this. Memory a region hands out stays in use until the region is freed, so a value replaced in a long-lived region (a flex pointer given a new new) keeps the old one's memory until then.

Static data: region global(N)

For programs requiring bounded static memory that lives for the program's entire duration, the main program file may declare a static global region with an exact byte capacity:

region global(1024)
    fixed base: int = 100
    flex counter: int = 0
    bind multiplier: int = 2
    hold state: State = State { active: true }
fi

Variables defined in region global are allocated in the static data segment and live for the entire program. Dynamic allocations may also be made into global (via new T(val) at global), and tasks can take global wherever a region is expected. Dynamic allocations that exceed the declared byte capacity trap at runtime with an allocation error. Variables in region global cannot hold dynamically growing collections (slices or maps) or pointers into shorter-lived local regions.

Tasks and regions

A task that allocates takes the region to allocate in as a parameter of type region, and the caller passes one of its regions by name. Its result can be a pointer into that region:

shape Node
    value: int
    next: maybe ^Node
fi

task push(r: region, list: maybe ^Node@r, value: int) -> ^Node@r
    <- new Node(Node { value: value, next: list }) at r
fi

task sum(list: maybe ^Node@r) -> int         ; only reads, so r is just a name
    when list is {node}
        <- node^.value + sum(node^.next)
    fi
    <- 0
fi

task main()
    region nodes
    flex list: maybe ^Node@nodes = none
    each i in 1..4
        list = push(nodes, list, i)
    fi
    show(sum(list))                        ; 6
fi

The region after a call. A task or a method that takes its region last can be given it after the parentheses, as new T(v) at r is: label(n) at r and label(n)@r are label(n, r). x.str() at r, text(x) at r, x.copy() at r, and scan(prompt) at r are the same rule. The two spellings differ in how much they cover: at r names the region of everything before it, and @r that of the call right before it.

task label(n: int, into: region) -> text@into
    <- "#{n}" at into
fi

task main()
    region r
    show(label(1) at r, label(2)@r, label(3, r))   ; #1#2#3
    show("a" + label(4)@r at r)                 ; a#4
    show(label(5) at r + "!")                    ; error: 'at r' names the region of everything before it, so it comes after the last part …
    region d default
    show(label(6))                            ; #6, in d
fi

A region named in a task's header without a region parameter, like r in sum, stands for whichever region the caller's arguments are in: sum(list) reads a list in nodes, and the same task reads lists in any other region. It costs nothing at run time, but the task can't allocate there (new … at r is an error that suggests the parameter). Two parameters with the same region name must be in the same region, and a result can only name regions the header's parameters name:

task make() -> ^int@r                     ; error: the result lives in region 'r', which the task isn't given
    region r
    <- new int(1) at r
fi

Regions aren't values: a region is only a region statement, a region parameter, an argument for one, or a name after @ or at. It can't be stored in a variable or a field, or sent from a task.

A block's default region

A block may name one of its regions its default, written after the region's name. Everything in the block that makes memory and names no region goes there, so at r is written only where memory goes somewhere else. A task takes one the same way, r: region default, which makes the region it is given the default of its body:

task grow(r: region default, upto: int) -> [int]@r
    flex made = new [int]()            ; in r
    each n in 1..upto
        made.add(n * n)
    fi
    <- made
fi

task main()
    region kept default
    fixed squares = grow(kept, 4)         ; a region is still passed by name
    fixed greeting = "Hello, " + "Ada"   ; text@kept
    fixed p = new int(7)                 ; ^int@kept
    each round in 0..3
        region pass default              ; this block's own default
        show("pass " + round.str())      ; text@pass, freed at the end of the pass
    fi
    region other
    fixed elsewhere = new int(1) at other  ; named, so it goes there
    show(squares.len, greeting, p^, elsewhere^)
fi

Shapes and kinds that hold pointers

A shape or kind whose fields hold pointers (directly, or in other shapes and kinds they hold by value) lives in a region too. Its declaration writes its pointers without a region, since they are in whatever region its value is, and a use names that region: Node@r. Everything one value holds lives in that one region, so reading a field of a Node@r gives a maybe ^Node@r.

kind Tree
    Leaf(value: int)
    Branch(left: ^Tree, right: ^Tree)          ; no region here: '^Tree@r' is an error
fi

task count(t: Tree@r) -> int
    match t
    case Leaf value
        <- value
    case Branch left right
        <- count(left^) + count(right^)
    fi
fi

task main()
    region trees
    fixed one = new Tree(Leaf(1)) at trees
    fixed two = new Tree(Leaf(2)) at trees
    show(count(Branch(one, two)))          ; 3
    show(count(Leaf(5)))                   ; 5
fi

A value built without any pointers, such as Leaf(5) or Node { value: 1, next: none }, takes the region of the place it goes: an argument, a field, or a variable with a written type. (So does the result of a task given only such values, as in first(none) for a task first(list: maybe ^Node@r) -> maybe ^Node@r, and a plan B on such a result takes the region of its fallback, which can't be a region declared in the plan B's own block, since that is freed when the block ends.) A binding without a type can't tell which region such a value should be in, so it asks for one:

flex head = Node { value: 1, next: none }     ; error: cannot tell which region 'head' lives in, ...;
                                              ;   give it a type, such as 'head: Node@r'
flex first: Node@nodes = Node { value: 1, next: none }
first.next = push(nodes, none, 2)                ; fine: first is in nodes

Text in a region

Text has a lifetime too. text lasts as long as the program, as a literal does, and text@r is in region r: text a value in r holds (an item of a [text]@r, a field of a Node@r, what a ^text@r points to) is in r, whatever it was made from. Text that lasts fits wherever text of a region is expected, and text of a region never goes where it could outlive its region: into a text, or text of another region.

A shape or kind holding text, but no pointers, may name a region or not. Person holds text that lasts, and Person@r may hold text in r; a literal takes the region of the text in it, and none when all of its text lasts. A shape holding pointers always names its region, and its text is in it.

shape Person
    name: text
    age: int
fi

; Takes a Person whose text is in any region, or lasts.
task greet(p: Person@r)
    show(p.name)
fi

task main()
    region r
    flex names = new [text]() at r
    names.add("Ada")                               ; a literal fits text@r
    fixed first = names[0]                          ; text@r
    fixed plain = Person { name: "Bob", age: 7 }    ; Person: its text lasts
    fixed held = Person { name: first, age: 36 }    ; Person@r: its text is in r
    greet(plain)                                   ; Bob
    greet(held)                                    ; Ada
    flex lasting = "none yet"
    lasting = first                                ; error: expected type text, got text@r; …
fi

Slices

A slice, [type]@r, is a view of items in region r: in C, a pointer to the first and how many there are. It follows a pointer's rules: it is never none (a maybe [int]@r can be), its type names its region except in a field and after new, it can't be stored in another region, and a task sends one only into a region it is given. new [T](items) at r makes a slice by copying the items into r: [v#n] for n copies of v, where n may be known only as the program runs, an array, or another slice. A slice made of an array or of another slice is a complete copy, as copy() is: where the items hold text, a slice, a map or a pointer, that is copied too, at every depth, so the new slice shares nothing with the one it is made of, which may be in any region. Assigning a slice, giving it to a task, or naming it again copies only the slice itself, and the items stay shared. A slice of the values written, [a, b, ...], holds those values as they are. new [T] at r (or new [T]() at r, see the empty value) makes one with no items.

Written by its values. [1, 2, ...] at r is the short form of new [int]([1, 2]) at r: the ... after the last value says the brackets make a slice, which can grow, rather than an array, and the type of the items is worked out from the values. @r names the region too ([1, 2, ...]@r), and in a block with a default region neither is needed. Both forms are the same slice, so either can be given, assigned (xs = [7, ...] at r makes xs a new slice), or passed. Where the values don't say the type, the long form does: [...] holds none (new [int] at r), and [none, ...] doesn't say what a value would be (new [maybe int]([none]) at r). An array never becomes a slice: [1, 2] where a slice is expected is an error that shows both forms, and [1, 2] at r is one too, since an array names no region.

flex xs = [1, 2, ...] at r      ; [int]@r, as new [int]([1, 2]) at r
xs.add(3)
xs = [7, ...]@r                  ; a new slice
show(total([10, 20, ...] at r))    ; a slice made to be passed
xs = ["a", ...] at r              ; error: expected type [int]@r, got [text]@r

A slice made to be passed or shown stays in its region until the region ends, as every value in a region does, although nothing reads it after its line (see Safety).

task squares(r: region, n: int) -> [int]@r
    flex out = new [int]([0#n]) at r
    each i in 0..n
        out[i] = i * i
    fi
    <- out
fi

task main()
    region r
    flex all = squares(r, 5)            ; 0 1 4 9 16
    flex middle = all[1..4]             ; 1 4 9: the same memory, not a copy
    middle[0] = 100                     ; a slice's items are written through it, and every view of them sees it
    show(all[1])                        ; 100
    show(middle.len)                    ; 3
    fixed copied = new [int](middle) at r  ; its own items
fi

Adding and taking out items

A slice's length can change; an array's can't, since it is part of the array's type. Four changes are built in:

task main()
    region r
    flex queue = new [text]() at r   ; no items yet
    queue.add("b")                       ; last: b
    queue.add("d")                       ; b d
    queue.add(0, "a")                    ; before item 0: a b d
    queue.add(2, "c")                    ; a b c d
    show(queue.del(1))                  ; b; now a c d
    show(queue.del())                   ; d, the last; now a c
    show(queue.len)                     ; 2
fi

How it runs. A slice also knows its room: after the first add, its items are in a block of the region with free space after them, and an add is a copy into that space. A full block is replaced by one twice as large, the items copied into it; when the block ends where the region hands out memory next, it grows in place instead, without copying. Old blocks stay in the region until it is freed, like any memory there, so a slice that grows to n items has used at most about four times their size in the region. del moves the items after the one taken out, and taking out the last item gives its space back to the room. To take the first item out without moving the rest, read it and slice: fixed first = xs[0], then xs = xs[1..xs.len].

Copies and parts. A copy of a slice (flex ys = xs) and a part of one (xs[1..3]) are views of the same items. Adding to one doesn't write over an item another counts: only the slice whose items end where the room begins adds in place, and the others copy their items into a block of their own first. Writing an item, del, and add in the middle move items in that shared memory, which the other views then see, as they see xs[i] = v. So does del at the end: it gives the item's space back to the room, and the next add to that slice goes there, over the item a copy made before the del may still count. The compiler warns where a task leans on this (see Warnings). After changing a slice, use the slice you changed, and give a copy that must keep its items of its own: xs.copy() at r or new [T](xs) at r, which both copy at every depth.

Maps

A map, [K: V]@r, holds a value of type V for each key of type K, found by the key's hash in about the same time however many keys it holds. Its table is in region r, and like a slice it names that region in its type except in a field and after new. [:V] is written for [text: V], the most usual map. It is made with new, from its pairs or none (new [:V] at r is empty, as new [:V]() at r is), or written by its pairs: ["a": 1, "b": 2] at r is the short form of new [text: int](["a": 1, "b": 2]) at r, with the keys' and values' types worked out from the pairs, @r naming the region too and a block's default region needing neither. A key in the pairs is any value, a variable too; in the [:V] form of new a key is a bare name, which is its own text, or text in quotes: new [:float]([width: 1.5, "full size": 2.0]). The same key written out twice is an error.

region r
flex ages = new [text: int](["ana": 31]) at r
ages["bo"] = 25                      ; adds the key
ages["ana"] = 32                     ; gives it a new value
show(ages["bo"], " ", ages.len)         ; 25 2
show(ages[#"cy"], " ", ages.has("cy"))  ; none false
show(ages.del("bo"), " ", ages)       ; { 25 } ["ana": 32]
each name, age in ages                  ; in the order the keys were added
    show(name, " is ", age)
fi
flex sizes = new [:float]([width: 1.5, height: 2.0]) at r
fixed names = ages.keys at r            ; [text]@r: ["ana"]
show(ages["zed"])                        ; stops: vela: no such key in the map …

Copying into another region

A value can't move to a region that outlives its own, but a copy can go there. value.copy() copies what a value holds in regions (its text, its slices' items, and what its pointers point to, all the way down) into the region of the place the copy goes: the slice it is added to, the variable or field it is assigned to, the parameter it is passed as (when the call shows that parameter's region), or the result it is sent as. Where nothing names a region (a fixed without a type, a comparison), the copy names one: value.copy() at r, or value.copy()@r.

shape Note
    title: text
    tags: [text]
fi

task main()
    region kept
    flex titles = new [text]() at kept
    flex first: maybe ^Note@kept = none
    each pass in 0..2
        region work                                 ; freed at the end of every pass
        fixed line = scan("Title: ") at work { "none" }
        flex tags = new [text]() at work
        tags.add(line)
        fixed note = new Note(Note { title: line, tags: tags }) at work
        titles.add(line.copy())                     ; into kept, the slice's region
        when first is none
            first = note.copy()                     ; the note, its title, its tags
        fi
        fixed shout = line.copy() at kept            ; nothing here names a region
        show(shout)
    fi
    show(titles[0])                                 ; Ada
    fixed kept_note = first { <- }
    show(kept_note^.tags[0])                        ; Ada
fi

Methods and regions

A value of a shape or kind that holds pointers or text lives in a region, and inside its methods that region is @this. A method may send what the value holds there, and take more of it; each call works the region out from the value, so one method serves every region.

shape Node
    value: int
fi

shape Named
    name: text
    node: ^Node

    nameOf() -> text@this              ; the value's own text, in its region
        <- .name
    fi

    alters point(to: ^Node@this)        ; a pointer into the value's region
        .node = to
    fi

    made(r: region, value: int) -> ^Node@r
        <- new Node(Node { value: value + .node^.value }) at r
    fi
fi

region r
flex n: Named@r = Named { name: "ada".copy() at r, node: new Node(Node { value: 1 }) at r }
fixed name: text@r = n.nameOf()
n.point(new Node(Node { value: 5 }) at r)
region s
n.point(new Node(Node { value: 9 }) at s)   ; error: expected type ^Node@r, got ^Node@s; method
                                          ;   'Named.point' takes 'to' in '@this', the region the
                                          ;   value it is called on lives in, so it must be in that region too

What a method sends from @this is text or pointers of the caller's region, so it can't outlive that region: kept = n.nameOf() into a text that lasts as long as the program is refused, as any text of a region is. A method is given the value, never its region, so it can't allocate in @this: new … at this, and adding to a slice the value holds, are refused, and a region parameter doesn't help, since the value can't point into another region. Grow the value where it is kept (n.node = new Node(…) at r, bag.items.add(v)), or have the method make something in a region it takes and send it back, as made does (an idea for later).

How it runs

A region is a list of chunks of memory. new takes the next bytes of the newest chunk (aligned for the type), so allocating is a few instructions; when a chunk is full, the next is twice as large, starting at 4 KB and growing up to 1 MB, and an allocation too big for that gets a chunk of its own. Freeing a region frees its chunks, however many values they hold. A region that never allocates costs nothing. In the generated C, a region is a local vela__rt__region r[1] (so r is a pointer to it, like a region parameter) and a vela__rt__region_free(r) on every way out of its block; the allocator is in runtime/vela_rt.h. If the system runs out of memory, the program prints vela: out of memory and stops.

What regions don't do (yet)

Modules

A program can be split into several files. Each file is a module, and a file includes another with include, naming its path without .vela. What the other file declares is then named through the module's name, the path's last part:

; main.vela
include shapes/geometry
include shapes/palette (Color, describe)

task main()
    fixed square = geometry.Square(3.0)
    show(geometry.area(square))              ; 9
    fixed corner: geometry.Point = geometry.Point { x: 3.0, y: 4.0 }
    show(corner.x)                           ; 3
    match square                             ; a case names the variant alone
    case Square side
        show(side)                           ; 3
    case Circle radius
        show(radius)
    fi
    fixed red: Color = palette.Red           ; Color came in by name
    show(describe(red))                      ; red
    show(describe(Color.Blue))               ; blue: a variant through its kind
fi

; shapes/geometry.vela
shape Point
    x: float
    y: float
fi

kind Figure
    Circle(radius: float)
    Square(side: float)
fi

task area(figure: Figure) -> float
    match figure
    case Circle radius
        <- pi() * radius * radius
    case Square side
        <- side * side
    fi
fi

priv task pi() -> float                ; only geometry.vela can call it
    <- 3.14159
fi
; in main.vela, after include shapes/geometry
show(geometry.pi())            ; error: 'pi' is private to module 'geometry'
show(area(square))             ; error: unknown task 'area'; module 'geometry' has one: write 'geometry.area', …

examples/shop.vela is a program in three files. In the generated C, a module's names are prefixed with its path (vela_shapes__1__geometry__0__area), so every module's names stay apart.

Vela's library. A path that starts with std names a module of the library that comes with the compiler, and is included like any other: include std/files {File}. It is read from the compiler, never from the disk, so a folder of the program's own can't be named std, and std is no reserved word: a variable may still be called so. It has five modules: std/collections, map, filter, fold and more over a slice's items; std/files, files on the disk; std/int512, 512-bit integers; std/loops, what a loop that gives a value fails with; and std/math, with math.P, math.e, powers, trigonometry, exp, and log. What the library has, and how it is made, is in Vela Library.

include std/files {File}

task main()
    show(File { path: "notes.txt" }.exists())    ; whether a file that can be read is there
    ; and open(), read(), line(), lines(), ended(), size(), write(), close(), rename(), delete(): see Vela Library
fi

Complete Example

task fib(n: int) -> int
    when n < 2
        <- n
    else
        <- fib(n - 1) + fib(n - 2)
    fi
fi

task main()
    each i in 0..10
        show(fib(i))
    fi
fi

Current Limits

Vela is young. These are known, and planned for (see Future Directions):

Building the Compiler

The Vela compiler is written in Rust and generates standard, portable C11, which it compiles with the C compiler named by the CC environment variable (cc by default). CC may include arguments, as in CC="ccache gcc".

cargo build
./target/debug/vela program.vela -o program
CC=clang ./target/debug/vela program.vela -o program

vela <input.vela> [-o <output>] [--emit-c | --check | --quick] [--no-notices] [--repeat-warnings] [--]: -o names the executable (a.out by default), --quick builds it without optimizing the C (-O0 rather than -O2): often several times faster to build, and slower to run, --no-notices builds a program that writes no notices to standard error, as of a startup decorator whose failure it lets pass (not with --check, which builds none), --repeat-warnings shows every place a warning said once for each task or file applies, not only the first (see Warnings), --emit-c prints the generated C instead of compiling it (or, with -o, writes it to that file; a program without main is allowed here), --check only checks the program (everything a build checks before running the C compiler, including that it has a main), -h/--help prints the usage, and --version prints the compiler's version. The files the program includes are read from the disk, next to it (see Modules). The output can't be the program's own file or a file it includes. A file name starting with - goes after --. A built program writes its output line by line to a terminal, and in blocks to a pipe or a file, which is much faster; it writes out what it holds before scan reads, before a file is read or written, and when a runtime error stops it. A program the system stops (a recursion too deep for the stack, or Ctrl-C) loses what it held, as a C program does. The exit status is 0 on success, 1 when the program can't be read or doesn't compile (in Vela or in C), and 2 for wrong arguments.

./target/debug/vela program.vela --emit-c -o program.c
cc -std=c11 -I runtime program.c -o program -lm   # the C includes runtime/vela_rt.h

Errors point at the exact place in your source:

program.vela:3:10: error: unknown variable 'missing'
 3 |     show(missing)
   |          ^

To run the test suite (requires a C compiler, $CC or cc):

cargo test
CC=clang cargo test

Tests are plain Vela programs: tests/run/ holds programs with their expected output (and a .in file as their input, when they read any), tests/fail/ holds programs the compiler must reject, tests/trap/ holds programs that must stop with a runtime error (such as an integer overflow or an index out of range) after printing their expected output, and every file in examples/ is checked against tests/examples/, as is every complete program in docs/regions.html against the output the page shows. The C generated for run tests and examples must also compile as strict ISO C11 with no warnings (-std=c11 -pedantic-errors -Wall -Wextra -Werror). Rust tests in tests/*.rs run those programs (programs.rs) and cover the command line, the library's build pipeline, the lexer, internal compiler errors, and the runtime (runtime.rs: the C tests in tests/runtime/, and the programs in tests/run/, tests/trap/, and examples/ run under AddressSanitizer and UndefinedBehaviorSanitizer with leak checks).

The compiler is also a Rust library, vela, so tools can use the language without copying it: vela::driver checks and builds programs exactly as the vela command does (and can keep a partly checked program for editors), vela::lexer::highlight splits source into tokens without stopping at errors (for editors), and lexer::KEYWORDS, ast::BUILTIN_TYPES, ast::BUILTIN_TASKS and ast::BUILTIN_KIND_NAMES list the language's reserved words and the names nothing else may take (ast::builtin_kinds gives those kinds as declarations, which Vela Studio reads).

The repository is a Cargo workspace: the compiler is the default member, and Vela Studio, the editor, lives under ide/. Plain cargo build and cargo test cover only the compiler, which has no dependencies; add --workspace to include Vela Studio's crates. Building and testing Vela Studio's page are described in its own guide.

cargo test --workspace

To lint the compiler (or everything, with --workspace):

cargo clippy --all-targets

The compiler's own code may not use unwrap(), expect(), panic!, or unchecked indexing (enforced by lints in Cargo.toml), so problems are reported as errors instead of crashes. An error starting with internal compiler error means a bug in the compiler itself, not in your program.

Design Notes