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
- Minimal surface. No classes and structs, no interfaces and traits: one of each. Shapes, kinds, and tasks, and patterns for what several shapes or kinds have in common.
-
One way to do it. Immutability by default with
fixed, at any depth; mutation explicitly opt-in, of the contents only withbind, or in every way withflex. - Safe by design. Static typing, exhaustive matching, no uninitialized variables, no null pointers, and pointers that can never outlive their memory.
-
Beautiful syntax. Words for control flow and logic
(
when,and,not), blocks closed withfi, no semicolons ending statements, and a symbol only where it reads as a picture:->names what a task gives back,<-gives it back, and|>passes a value on to the next call. -
No GC, no free. Values live on the stack, and
newallocates in a region, which frees everything in it at once when its block closes (see Memory and Regions).
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).
vela-studio examples.
Types
int— 64-bit signed integerfloat— 64-bit floating pointbool— true or false-
unit— a whole number from 0 to 255 in one byte (see Units) -
int8,int16,int32,uint16,uint32,uint64,float32— numbers of a size of their own;int64is another name forint,uint8forunit,uintforuint64, andfloat64forfloat(see Number sizes) -
char— one byte, a character, written'a'(see Chars) -
text— UTF-8 text that knows its length,t.len, in bytes;==compares contents.textlasts as long as the program, as a literal does, andtext@ris in regionr(see Text in a region) -
nothing— the return type of a task that sends no value (a task with no->has it); it has no values, so it can't be the type of a variable, parameter, or field -
^T@r— pointer to a T in regionr; it always points to something (see Memory and Regions) -
[T#n]— an array:nvalues of type T, held in place (see Arrays) -
[T]@r— a slice: a view of values of type T in regionr(see Slices) -
[K: V]@r— a map from keys of type K to values of type V, its table in regionr;[:V]is[text: V](see Maps) -
(T, U)— a tuple: two or more values held together, whose items never change (see Tuples) -
maybe T— a T, ornone(see Maybe and Errors) -
T ~ E— a T, or an error E, sent by a task that can fail;~holds the whole type before it, somaybe int ~ textis(maybe int) ~ text, and parentheses group the other nesting,maybe (int ~ text)(see Becoming a wider type) -
region— the type of a task's region parameter,r: region -
T— a type a task, shape or kind takes, standing for the type each use gives it (see Types a declaration takes) -
Box<int>— a shape with the types it is given, in the region its values live in when it holds anything there (Box<text>@r) - User-defined shapes and kinds, with methods of their own (see Methods), and kinds that imitate others, having their variants (see Kinds that imitate kinds)
-
Name— a type alias, another name for a type,alias Name => type(see Type aliases) -
Patterns, which say what a shape or kind following them has, and
which a type a task takes may be asked to follow,
<T: Shaped>(see Patterns)
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:
-
A value that never changes: a
fixed, a plain parameter (which a task passes on by taking it asflex&itself), a result, a tuple's item, areadfield outside its shape's alters methods, and a value made at the call (&(a + 1)). -
An item of a slice:
&xs[0]. A slice's items move to new memory when it grows, and the task would go on changing the old copy; give the slice and the index instead. -
The mark where it doesn't belong:
&agiven to a parameter that takes a copy says to leave out&, andagiven to aflex&one says to write&a. Beside these, only afixed¶meter, a reference, and an address take&a. -
One place given twice where the call changes it:
swap(&x, &x),f(x, &x),c.give(&c.n)(wheregivealtersc), andswap(&items[i], &items[1]), whose two items may be the same. The task would see a value change that it never wrote. A copy is read before the call starts, so a method may still be given a copy of the value it changes, or of a part of it:c.grow(c.n). Two different fields or two different items written as numbers are fine, and so are two places through pointers,&p^and&q^, which can't be told apart before the program runs, as writing through two pointers can't. -
Another type: the place holds exactly the
parameter's type, since the task may write any value of it there.
maybe intis not given anint, and aflex& t: text@pgiven text that lasts as long as the program is refused when another argument makespa region that ends sooner.
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:
-
fixed x: Tis always a copy, a place of its own, even of a large value. The call gives any value, as for a plain parameter. -
fixed& x: Tis the caller's place, only read: never copied, and never changed throughx(the place itself may still change another way, through a pointer or a shared field, as Safety lists). The call marks it,f(&a), as forflex&, and gives any place afixed&reference may name: a variable (afixedtoo), a field or an array item of one, what a pointer points to, or a parameter, passed on.
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:
-
An
addor adelon aflexslice parameter, the task's own copy of the caller's slice: what the task writes afterwards may or may not reach the caller, and adelmoves items the caller sees. The warning says to take the caller's place,flex& xs: [int]@r, or to work on a copy of its own,xs.copy() at r. -
A slice made of another in the same task, a copy
flex ys = xsor a partxs[1..3]: an item written through either after anaddon one, and either used after adel(with an index) on the other. Reading only.lenis never warned of.
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:
-
It is not a type.
yabove is anint, likex, and of exactly its place's type. Only a block's name or a parameter is a reference: never a field, an item, a result, or a value sent with<-, andflex z = ycopies whatynames. So a reference never outlives its place, which is declared before it, in its block or one around it. -
It is never pointed elsewhere.
y = 5writes to the placeynames, andy = &zis an error that says so; the compiler always knows what each reference names. -
A call sees through it.
swap(&x, &y), whereynamesx, gives one place twice, and is refused asswap(&x, &x)is. -
It is declared one at a time,
flex& a, b = …being an error, andflex&is one word, as for a parameter.
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:
-
All or none. Either every result has a name or none
does:
-> q: int, intis an error. -
The names are the task's. A result can't share a
parameter's name, and the body can't declare another variable with
it. After it is set it reads like any variable,
total <- total - 1. -
Only
<-changes a result.total = 0reports 'total' is a result; set it with 'total <- …', andcount <- 1on an ordinary variable reports 'count' isn't a result of this task; assign it with 'count = …'. So every line that changes what a task sends starts with a result's name and an arrow. -
No zero values. Vela has no uninitialized
variables, and results are no exception: each must be set before it
is read, before every bare
<-, and before the end of the task, on every path. Reading one too early reports 'peak' might not be set here, and a path that reaches the end without setting one reports task 'collatz' can finish without setting 'peak'. A<- valuesline sends its own values, so nothing after it runs and nothing else need be set. -
Whole values.
best <- Point { x: 0, y: 0 }sets a shape result; setting one field of it,best.x <- 0, isn't part of this. -
A task that sends
nothing ~ Esucceeds by reaching its end, so its result has no value to name.
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:
-
The items of an array or a slice, the fields of a shape, and what a
pointer points to may be assigned,
scores[0] = 10,p.x = 1,ptr^.x = 1; a field may be assigned whole, since it is contents. -
A slice keeps its length:
addanddelchange the slice itself, so they are refused;xs.added(v) at randxs.without(i) at rmake a new one with the change (see A new slice with one change). A map is one table whatever its keys, som[k] = vandm.del(k)are allowed. -
An
altersmethod of a shape may be called on it, but not one of a kind, which may replace the whole value (.this = Green). -
A value with no contents, a number, a
bool, achar, text, a tuple, a kind or a task value, has nothing to change, so abindof one acts asfixed. -
It can't be given what a fixed name owns (see
What a fixed name owns), and it isn't
given to a
flex&place, which may assign it as a whole; abind&one takes it.
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:
-
Whole: numbers,
bool,char,unit, text, and arrays, tuples, shapes and kinds made only of these. A copy is a value of its own, so changing it changes nothing else. Text may live in a region, but it never changes, so it is whole too. -
Twofold: a slice, a map, a pointer, and anything
that holds one, such as a shape with a slice field or a
maybepointer. It is in two parts: what a variable holds (where the items are, and for a slice how many), and the items, in a region. A copy copies the first part, so it names the same items. A lambda whose call may send back a twofold value is twofold too. -
Place: where a value is kept, a variable, a field
or an item of one, or what a pointer points to.
&xgives the place itself, not a copy of its value: to aflex&orfixed¶meter, to a reference,flex& y = &x, or to a lambda keeping it.
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:
-
a
flexvariable or name, or one assigned:flex ys = xs,ys = xs, aflexname in a case form; -
a
flexparameter:sort(scores), wheresorttakesflex items: [int]@r; -
a place that changes: a field, an item, a map's value, a slice added
to (
bag.items = xs,all.add(xs)); - a task's named result (
out <- xs); - the copies a
[flex @r]lambda keeps.
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:
-
An
intraised to aunit, or to a constant 0 or more (n ** 2,n ** SIZE), is anint, computed exactly, and a result too large for anintstops the program as*does (integer overflow). -
An
intraised to any otherintmay be a fraction (2 ** -1is 0.5), and whether the exponent is negative is known only when the program runs, so the power is afloat:2 ** kis a float even whenkis 3. -
A power with a
floaton either side is afloat, computed by C'spow:9 ** 0.5is 3, and(-8.0) ** (1.0 / 3.0)is nan. Here anintand afloatmix, as they do in no other arithmetic. -
A
unitraised to aunitis aunit, and one past 255 stops the program (3 ** 6 does not fit in a unit). A number written out as the exponent is a unit there (b ** 2); a unit raised to anintvariable asks forint(b) ** norb ** unit(n).
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:
-
A number written out becomes one where one is expected (
flex n: int8 = -5, an argument, a field, an item) and beside one (n + 1,n < 100), and one that doesn't fit is an error (300 doesn't fit in an int8, which holds -128 to 127). A float written out becomes afloat32the same way (f * 2.5). -
Two numbers of different types don't mix:
int32(a) + bora + int8(b)says which (an int8 and an int16 don't mix in arithmetic …). -
Arithmetic,
++,--and+=give the same type, and never wrap around: a result that doesn't fit stops the program (vela: 128 does not fit in an int8 at 5:11 in task 'main', vela: integer overflow … for auint64). An unsigned number is never negative, so-non one is an error. -
**raises a sized number to a power of its own type, checked to fit; a signed one's power is a constant 0 or more, since a negative one makes a fraction. -
A conversion,
int8(x),uint64(x),float32(x), stops the program when the value doesn't fit, asunit(x)does;int(n)of auint64past the largestintstops it too. -
They compare, order, show and turn into text as numbers do, are keys
of a map, and a generic's
orderedtakes them.std/int512hasInt512, a 512-bit signed integer with full checked arithmetic methods and text formatting (see std/int512).
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.
-
A shared field never changes unless it is
shared flex, which any code that sees it may change, orshared bind, whose contents any code may change,Counter.box.value = 5, but which is never assigned as a whole, not by its shape's methods, nor through aflex&. A value decides whether its own fields change (fixed porflex p), and no value owns a shared field, so its declaration says it. -
shared readis read anywhere and changed only by the methods of its shape, andpriv sharedis seen only by its own file, as for any field. The words go in that order:priv shared flex f,priv shared bind f,shared read f. - It always names its first value, one the compiler works out without running the program, as a field's value to fall back on is. That value may name a shared field above it in its shape, or one of another shape; ones that name each other in a circle are refused. A field's value to fall back on may name a shared field that never changes.
-
Changing one needs no
alters, which is about the value a method is called on: an ordinary method, or a task, changesPoint.madeas it may. -
It lives as long as the program, which no region does, so it can't
hold anything that lives in one: a pointer, a slice,
text@r, a value holding one, or aFile. Numbers,bool, text written in the program, arrays, tuples,maybe, kinds, and shapes of those are all fine. (See a static region for an idea that would lift this.) - A shape that takes types has no shared fields yet (see the idea).
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.
-
It is never
alters, which is about the value a method is called on. It changes ashared flexfield as a task may, and its shape'sshared readfields, since it is one of the shape's methods. -
An ordinary method calls one the same way,
Point.new(…), and one called on the name is always shared:Point.area()is an error that saysareais called on a value, andp.new(1, 2)one that says to call it onPoint. -
priv sharedis seen only by its own file. A shared method shares one namespace with the fields and methods of its shape, and a kind's with its variants, sinceKind.Name(…)makes a variant. -
strwrites the value it is called on, so it is never shared, and a shape that takes types has no shared methods yet, as it has no shared fields.
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.
-
Fields first. Every field or variant goes above the
methods:
every field of a shape goes above its methods, and method 'total' is declared at 5:5. -
Names. A field and a method are both written
.name, so they can't share a name, and neither can be calledthis. No method can be calledadd,del,copy, orlen, which every value or slice already answers to. A method calledstrturns the value into text forx.str() at randtext(x) at r, so it is writtenstr(r: region) -> text@r, neveraltersand neverpriv, and replaces the text the shape or kind has without it (see Values as text). A method's name clashes with nothing outside its type, so two shapes can each have atotal.altersandthisare not reserved words: a variable can be called either, as it can be calleddefault(see A block's default region). -
privmarks a method only its own file may call, as it marks a field. -
No automatic dereference, as with fields: a method
of what a pointer points to is called
p^.total(). A value of a type without methods says so:type int has no methods. -
Shapes that take types. A method of
shape Box<T>usesTas its fields do, and one is made for each set of types the shape is given (see Shapes that take types). -
Types of its own. A method may take types of its
own, written after its name as a task's are,
apply<R>(f: (T) -> R) -> R, with the words a type must allow,with<A: shown>(…); they come after the shape's or kind's, and none may have the name of one of those. A call's values tell them, or the place its value goes (fixed x: maybe text = b.nothing_of()), as for a task; they aren't written at a call yet, sinceb.apply<int>(…)would have to say whose types it lists. One copy is made for each set of types used, as for a task. A pattern may ask for such a method too, and a follower names its own types as it likes, as it names its parameters:give<S>(…)is whatgive<R>(…)asks. -
Regions. The region the value lives in is
@this, so a method can send what the value holds,nameOf() -> text@this(see Methods and regions). -
A message or a runtime trap inside a method names it as the program
writes it:
in task 'Counter.step'.
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
-
Items. A literal's values have one type, except
that
noneand a value make amaybe([none, 7]is a[maybe int#2]), andfail eand a value make aT ~ E. A literal of onlynoneneeds a written type, as infixed slots: [maybe text#4] = [none#4]. Arrays go anywhere a type does: fields, payloads, parameters, results,maybe [int#2], and arrays of shapes (points[1].x = 4). An array holds its items by value, so a shape can't hold an array of itself, and an array of pointers lives in a region like a shape holding them ([^Node#3]@r). An array keeps no region of its own, so the one written on it is its items' ([text#2]@rand[text@r#2]are one type, and so is[text@r#2]@r); writing two different ones is an error. -
Count. A count is at least 1 and known before the
program runs: a number in a type, and a number or arithmetic on
numbers (
[0#2 * 8]) in a literal. For a count known only while the program runs, make a slice in a region (see Slices). A type whose values would take more than 1 TiB of memory is an error. - Index. An index out of range stops the program with a message saying where, on every C compiler (vela: index 4 is out of range 0..4 at 7:12 in task 'main'); a constant index out of range is an error when compiling.
-
Safe index.
xs[#i]reads the same item in amaybe:{ item }wheniis in range,nonewhen it isn't, so it never stops the program. It works on arrays, slices, and text (amaybe char, ast.at(i)is), and a loop can walk items without their count. It gives a value, not the place of the item, so it is never assigned (xs[i] = vis), and a part isxs[a..b], without#. -
each.
each v in xsgoes through the items in order, as they were when the loop started: an array is copied, and a slice is gone through where its items were then, as many as there were, even if the loop adds or deletes some (a deleted item is still read).each v in &xsgoes through the placexsas it is at each pass instead: each item is read through it when its turn comes, and the loop ends wherexsends then, so an item deleted is never read and one added is gone through too. The place is a variable, a field or an item of one, or what a pointer points to (each v in &b^.items); the loop changes nothing through it.flex xs = new [int]([1, 2, 3]) at r each v in &xs show(v) ; 1, then 2: the del leaves one item after it xs.del() fi
-
Written whole.
show(xs)writes[1, 2, 3], and so doesxs.str() at r, as it does for a slice and a tuple ((1, "a")). -
Compared.
==compares two arrays by where they are and~=item by item (see Comparing). - Not yet. A part of an array is a slice, which points into a region.
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
-
Items. Only values that can't change: numbers,
bools, text, other tuples,
maybeandT ~ Eof those, and shapes and kinds whose fields are all such. A pointer, a slice, or an array (or a shape holding one) is an error, since what it holds could change.(x)is only parentheses, so a tuple has at least two items, and a tuple is written on one line. The same holds for types:(int)isint, which is how a type is grouped (Becoming a wider type), and()and(int,)are errors. -
Taking apart.
fixed a, b = tandfixed (a, b) = tdeclare copies of the items; a nested part is written in parentheses (fixed a, (b, c) = t), a name may have a type (fixed n: int, (f: float, s: text) = t), and_drops an item. Changingalater leaves the tuple as it was. An assignment (x, y = t,(x, y) = t) writes into existingflexvariables, whose types must be the items'. -
Several results are not a tuple.
-> int, intsends two values, and-> (int, int)one tuple:fixed qr = divmod(17, 5)stays an error for the first, and<- a, bis an error for the second, which says to write<- (a, b). Values received from several results can be put in a tuple:fixed qr = (q, r). -
Regions. An item of text names its own region,
(int, text@r), or the tuple names one for all of them,(int, text)@r; the same one written on both is the same type, and a different one is an error. Two items may be text of two regions,(text@r, text@s). In a field, a slice, or afternew, items name none: they are in that value's region. -
Comparing.
==and!=compare two tuples by where they are, and~=item by item, text by its characters (see Comparing).<and the other order comparisons are for numbers, so compare items (t.0 < u.0). -
Where they go. Anywhere a type does: parameters,
results, fields, payloads, arrays and slices (
[(text, int)]@r, whose items are replaced whole,xs[i] = ("Cy", 80)),maybe, and afternew. -
Case forms. In
isandcase, braces take the value out of amaybeor aT ~ Eand parentheses take a tuple apart, sowhen found is {(name, age)}does both, as docase {(a, b)}andcase failure (line, message). A plain tuple has no other case, sowhen t is (x, y)always matches: its block runs with the items named, so a<-in it sends for certain, and anelseon it, aloopof it, or amatchon a tuple, is an error that says to usefixed.flexbefore a name makes it a variable the block can assign (is flex (x, y),{flex v},case Circle flex r), andbindone whose contents it can change (is bind (x, y),{bind v}); either is a copy, so changing it leaves the value matched as it was, but for what a twofold copy names.
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
-
Which type a call is for. From its arguments, from
the place its value goes (a binding's written type, a parameter, a
field, a
<-, or a plan B's value), or written in< >after the name, as in the header. Writing them is all or nothing: a task that takes two types is given both. When nothing says which type it is, the compiler asks for it:can't tell which type 'T' is for this call of 'empty' …. -
Reading
<. A<after a name starts the types of a call only when the matching>is followed by(, soa < bis still a comparison. (a < b > (c)was never valid Vela:>doesn't compare bools.) Two comparisons joined by a comma can read that way too:(i < n, n > (m))is a call ofigiven the typesn, n, and the error says so wheniis a value. Parentheses around either comparison make them two,((i < n), n > (m))or(i < n, (n > (m))), since parentheses in types pair up and hold no>of their own. -
Regions. A type a task takes names no region of its
own: what a
Tholds is in the region of the type given for it, soT@ris an error. A slice or pointer around it names one as usual ([T]@r), and then the type given must live in that region — a call that would keep a value of one region inside a value of another is an error that says to copy it first. A written type names its regions, as a written type does everywhere else:first<text@r>(names), notfirst<text>(names). -
Copies.
v.copy() at rworks on aT, whatever it turns out to be. A copy lives where it is made, so it is aTonly where the header already keeps aTin that region — as a parameterxs: [T]@rorp: ^T@rdoes, since a use must then give a type of that region. A copy into any other region (one the task declares itself, or one nothing ties toT) is only the value itself, so every use must give a type that holds no text, slice, or pointer; otherwise the compiler says so, naming the copy. -
Shapes, kinds, and modules. Any type fits a
T: numbers, text, tuples, arrays, slices, pointers,maybe, shapes, and kinds. A task that takes types can be public orpriv, used from another module (numbers.larger(3, 8)), and can use another such task, giving it the type it was given itself. A name in< >can't be a builtin type or a type the file already names, andmaintakes no types. -
Kinds too. A kind takes types the same way,
kind Option<T>(see Kinds that take types). A call that gives a type built from its own (f<(T, int)>insidef<T>) would never stop, so a type built up more than 24 types deep, or a task used with more than 500 sets of types, is an error.
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
-
Literals.
Box { value: 1 }works out the types from the values it gives, as a call does from its arguments;Box<int> { value: none }writes them, for when a value says nothing (none,fail e). Writing them is all or nothing, and a shape that takes no types has none to name. -
Where they go. Anywhere a type does: a field, a
parameter, a result, an array or slice
(
[Box<int>]@r), a tuple, amaybe, a kind's payload, afternew(new Box<int>(Box { value: 1 }) at r), and inside another shape's types (Box<Box<int>>,Pair<Box<text>, int>@r). A declaration that takes no types of its own may name one, as atask textOf(b: Box<text>@r) -> text@rdoes, since the shape made for a set of types is one type with the shape given them. A shape that takes types is not a type on its own:Boxalone is an error that says to name them. -
Regions. A type given names no region
(
Box<text>@r, notBox<text@r>), and a shape given types names one exactly when its values hold something there:Box<text>@randBox<int>. Inside a task that takes types, where the type given isn't known yet, that can't be told apart, soBox<T>names a region (b: Box<T>@r) and the region is dropped for the uses that don't need one, which leaves a task sending one back taking a region some of its uses have no use for (an open question). -
Recursion. A field may name the shape again with
the types it takes, behind a pointer (
next: maybe ^Node<T>), as a shape may name itself. One that gives it a type built from its own (^Grow<Grow<T>>) would need an endless line of shapes, so that is an error after 24 of them, as is a shape containing itself by value. -
Words. The same five words, meaning the same
things:
shape Shown<T: shown>may only be given a type that word allows. A task passing its own type on must ask for at least as much: atask tell<T>(b: Shown<T>@r)is an error that says to requireshowntoo.
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
-
A variant's types.
Option.Some(1)works out the types from the values it carries, as a shape literal does. Where they don't say every type, asOption.Nonecarries none, the place the value goes says the rest, as it does for a barenone: a typedfixed, an assignment, a parameter, what a task sends, a field, an error side (<- fail Option.Nonein a task sendingint ~ Option<text>), and a value of the kind beside it in an array, in the branches of awhenor amatch, or across==. A wholewhenormatch, and a tuple, take them from the place too:<- when c Option.None else Option.None fiin a task sendingOption<int>, and a branch'sfail Option.Nonetakes them from the error side. Values side by side may each say some:[Either.Left(1), Either.Right("x")]is an[Either<int, text>#2], and in a shape literal one field's variant takes them from the fields beside it, as inHolder { v: Option.Some(1), q: Quiet.Silent }. Where nothing says them, they are written after the kind's name,Option<int>.None,Either<int, text>.Right("r"): all of them or none, never after the variant, and with the.touching the>, which tells them from a comparison with a field of.this, as in(a < b, c > .x). A value whose types nothing says, or a method called on one, is an error that shows how to write them. -
Matching. A
casetakes the types from the value it matches, so it names none:case Some vorcase Option.Some v, andvis of the type the value was given. In a task that takes types, acaseon anOption<T>binds aT. -
The rest, as shapes have it. A kind given types
goes anywhere a type does, and
Optionalone is an error that says to name them; a type given names no region, and the whole names one exactly when its values hold something there (Option<text>@r,Option<int>); a variant may name the kind again behind a pointer (Node(left: ^Tree<T>, right: ^Tree<T>)), and one giving it a type built from its own is an error after 24 kinds; the five words ask the same of the types given. Its methods see the types too (orElseabove sends aT), its ownstrwrites every kind made from it, and==and~=compare by what the kind made carries. As for shapes that take types, it has no shared methods yet, and its methods take no wrappers yet.
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
-
Where its values go. A value of the kind imitated
goes where one of the imitating kind is expected, since every
variant it can be is there, carrying the same values: a name with
that type, an argument, what a task sends, an error a task sends
(
<- fail eabove), a field, an item, a value beside one of the imitating kind ([Empty, OrderError.TooMany(4)]is an[OrderError#2]), and either side of==or~=. It is made a value of that kind there, the same variant carrying the same values, which costs a copy. Never the other way round, since a value of the imitating kind may be a variant the other doesn't have (amatchmakes one from the other), and only the value itself: a value already made is never remade inside (see Becoming a wider type), so amaybe ReadErroror a[ReadError]@rstays what it is. -
Naming its variants. A variant's own name names the
variant of the kind it is declared in, as it always did, so
Emptyis aReadError; the imitating kind's is named through it,OrderError.Empty. Acasenames variants of the kind of the value it matches, socase Emptyon anOrderErroris its own. None of its variants may be named like one it has from a kind it imitates, and a shared method can't be named like any of them. -
Several, and from anywhere. A kind may imitate
several, joined with
&(kind AppError::ReadError & files.FileError), as long as no two of them have a variant of the same name, and each once. The kind imitated may be another file's (errs.Missing, but not one it keepspriv), Vela's library's, a builtin one (kind BadNumber::ParseError), one that imitates others, whose variants all come along, or a type alias of one. No kind imitates itself, through others or not. -
Kinds that take types. One that takes types is
imitated with them given:
kind Counted::Option<int>, orkind Choice<T>::Option<T>, whose variants carry whatOption's carry, withChoice'sTin place ofOption's; anOption<int>then goes where aChoice<int>is expected. The kind imitated names no region, since the variants it gives name none.
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
-
Methods a follower writes. A method's header alone,
area() -> float, is one every follower writes, with the same header: the same parameters, each taken the same way, the same results, andalterswhere the pattern says it; the names of its parameters and of the regions it takes are its own. A follower that lacks one, or writes it otherwise, is an error that shows the header to write. Several headers may share a line, parted by,, asstart(), stop()above. -
Defaults. A header followed by a body, closed by
fi, is a default: each follower has it as its own method unless it writes one of that name, asCirclewritesdescribe. A line holding one header is followed by its body when the next line starts no other member, and a barename()on the next line counts as a line of that body; to ask for two methods with no values, write them on one line,start(), stop(), which the message for a misread one says too. A default's body has at least one line (<-for one that does nothing). It is checked once, as a task taking the type following the pattern, and again in each follower, as that follower's own method; it reads the value as.this, its fields as.name, and calls the pattern's methods as.area(). -
this. Inside a pattern,this, and the pattern's own name, are the type following it:larger(other: this)takes a value of the follower's own type, andsame(other: Shaped)means the same. In a follower that lives in a region, it is in@this, the region of the value the method is called on, sootherlives there too. -
Fields. A field in a pattern,
name: text, is one each follower declares itself, of the same type and notpriv, so a shape's whole layout stays in its own block (a literal gives every field, and every field is there to read); the pattern names no value it falls back on, which the shape may. A kind has no fields, so it follows only a pattern without them. -
In a task taking types.
<T: Shaped>lets the body call the pattern's methods on a value ofT, checked against its headers, and read its fields that hold nothing in a region (anint, say; a value ofTlives where the task doesn't know, so text is read through a method). Each use gives a type that follows the pattern, and the task made for it calls that type's own methods, directly, as any call: a pattern costs nothing when the program runs. A pattern joins the words with&,<T: Shaped & shown>, and a shape follows several the same way,shape Circle::Shaped & Counted, as long as no two ask for one name; a kind lists patterns among the kinds it imitates,kind Light::Shown. -
Following is said, never guessed. A shape that
happens to have an
area()but doesn't write::Shapeddoesn't follow it, as in Rust, so a task asking for a pattern is given only what was meant for it. A pattern is a name of its file, as a shape is:m.Shaped,include m (Shaped), andpriv pattern; a shape that takes types follows one too, as inshape Box<T: shown>::Shown. -
Types a pattern takes. A pattern may take types, as
a shape does,
pattern Iterator<T>, with the words they must allow,pattern Ranked<T: ordered>; its fields and methods name them. A shape or kind following it gives them,shape Countdown::Iterator<int>, one for each, and is asked for what the pattern asks with them in place:alters next() -> maybe int. A shape that takes types may give one of its own,shape MapIter<I: Iterator<T>, T, R>::Iterator<R>, and one whose items live where its value does says so,shape Words::Iterator<text@this>, withalters next() -> maybe text@this. A type follows a pattern once, so::Iterator<int> & Iterator<text>is refused, and the defaults it has from the pattern read the types it gave. A task asks for one with the types as types of its own,task first<I: Iterator<T>, T>(flex it: I) -> maybe T, in any order:Tis worked out from what the type given forIfollows, never from a value, sofirst(countdown)sends amaybe int, and the same holds for a shape's literal. A bound gives each type what the pattern asks of it:<R: Ranked<T>, T: ordered>. -
Not yet. A pattern follows no other pattern, has no
shared methods and gives its methods no decorators or values that
parameters fall back on, and its defaults hold no lambdas. A builtin
type such as
intfollows none, since nothing writes that it does. And a pattern is no type of a value: a slice of different shapes that follow one, called through it as it runs, is an idea for later (see Patterns as the types of values).
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
-
The order. A value may call a task with wrapper
values of its own, and those are worked out first, so none is read
before it is set. A value that calls its own task, itself or through
others, would read what isn't set yet, and values whose tasks call
each other have no order to be worked out in: both are errors that
name the calls. A call of a task held as a value may reach any task
named as a value in what can run where the program starts (a shared
field's first value, these values, and the tasks they call), so a
value making one counts as calling each of those; a task named only
later, by
mainor a startup decorator, is not. -
Read once. A
shared flexfield a value reads is read where the program starts, so a later change to it, a startup decorator's too, is not seen by the wrapper. For the same reason a value is no place, so a wrapper can't take one asflex&orfixed&. -
Failing and stopping. A value that can fail is read
with a plan B, as anywhere (
int(t) { 1 }), and a<-in one is an error, since it is in no task to leave. One that stops the program stops it beforemain, with a message naming the wrapper and its task:vela: division by zero at 9:14 in wrapper 'counted' on task 'sq'(a decorator's values are named the same way,in decorator 'route' on task 'home'). - No region. A value living in a region (joined text, a slice, a pointer, or a value holding one) waits for a region that lasts as long as the program (see Future Directions).
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).
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
-
Where they go. Above
shared m(…)orpriv shared m(…), as above a task. A method of every value takes no startup decorator, since it has no value to be called on where the program starts, but it takes wrappers (see wrappers on methods). A shared method of a shape that takes types doesn't exist yet, so neither do its decorators. - The order. A shared method's startup decorators run at its place in the file, among the tasks, in the order the program is written.
-
Its rules are a task's. A shared method that is a
decorator takes the task first, and one that is a wrapper the rest
of the chain; one decorated takes a decorator of its type, as one
taking a place or a region does (see
places and regions). A
priv sharedone is named only in its own file. A wrapper on a shared method is no method of its shape, so it changes none of the shape'sshared readfields; the method itself, called as the rest of the chain, does. -
Messages name it as it is written:
vela: decorator 'checked' failed on task 'Math.half': …, and a runtime stop in itin task 'Math.half'.
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
-
The start. Its names come in two kinds. A name
already declared, and changeable, is set and keeps its last value
after the loop:
loop i = 0 || …. A name of the loop's own is declared withflex,bindorfixed, which covers every item after it, so names already declared come first:loop i = 0, j = 0, flex x = 2, y = 3 || …setsiandjand declaresxandy. Those live only in the loop, and afixedone, asgapabove, is worked out once and never changes. A loop never declares a name by itself:loop i = 0 || …with noiis an error that says to writeflex, and so isloop x < 10, as everywhere. A name the loop declares and no part of it uses is an error. Items run left to right, so one may use the names before it. -
Items and commas. A
,ends an item only where the next one starts, soa, b = b, a + bandflex q, r = divmod(a, b)are one item each, as they are on a line of their own; nor do the commas of brackets, of the types a use is given (Either<int, text>.Left(3)), or of a lambda part items. The start and the step stay on the loop's line, so awhenormatchin an item closes withfithere. -
The condition. One, or none: a loop with none never
ends by itself. It is any condition a loop takes, an
isand parts joined withandtoo, and it is tested inside the loop, so a plan B there may stop it. It alone may go on past the line, as amatchgiving abooldoes, and then the step follows itsfi:fi || i++. A start that only gives a value, as inloop a || b, is an error that says Vela writesor. -
The step. Any statements, a call or a one-line
when … fitoo, run after each pass.continuegoes to the step, as in C, andbreakleaves without it. So a step has nocontinueof its loop, and declares no names, which would end before anything could use them. With no step, the last||may be left out:loop flex w = 0 || w < 2. -
Endless.
endlesstakes nothing on its line.loop || ||,loop ||,loop trueandloop || truework the same, with a warning to writeendless(see Warnings). Either can be named,outer => endless. One that nothing in it stops never finishes, so a task that ends with it sends from inside it, with no<-needed after it. -
Not 'or'.
||parts a loop's parts and nothing else: anywhere else it is an error that says Vela writesor.
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
-
Without a name, the innermost loop.
break outerstops the loop namedouter, which a loop inside it can't do otherwise. A loop is named with=>before its word,outer => loop …orouter => each …, and only loops are named so; a loop that gives a variable its value is named by it (see Loops that give a value). The name follows the rule of every name: it matches none in scope where it is given, nothing inside the loop may take it, and loops side by side may each use the same one. A name that is no loop's around it is an error that lists the named loops there. -
A loop's own tests. A
looptests its condition inside itself, so abreakin a plan B there stops that loop, as inloop xs[#i] { break } > 0. What aneachgoes through is found before it starts, so abreakthere stops the loop around it. -
Regions are freed on the way out. The regions
declared inside the loop so far, in its body or in a block inside
it, are freed before the jump, as at the end of a pass, the way
<-frees the task's. -
Instead of a value. A plan B, or a branch of a
whenor amatchused as a value, may stop the loop or go on with it instead of giving the value, as it may send with<-(see A match or a when as a value).breakis a statement, so such a case is written as a block:case Halt = breakis refused. Ashowwhose value stops the loop has written the values before that one, and leaves the line unended, as a<-there does. -
In its own task. A lambda's body is a task of its
own, so a
breakin it can't stop a loop around the lambda, nor name one, and a name inside it can't be a loop's around it. In a task taking a group of types,each v in argsis written out once per value, and is still one loop. -
A value.
break = vandbreak outer = vstop a loop that gives a value with one (see Loops that give a value); in any other loop abreaktakes only a name.continuenever takes a value.
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
-
Single-value form. A loop giving one value has a
finallyblock beforefi. It runs once, after the final pass, and its last line gives the value. It is outside the loop: what it declares ends with it, and abreakor acontinuein it is the loop around it's. Its passes giving values too is a warning, asfinallyoverrides them. -
Map form. A loop without
finallywhose passes end with a value gathers them, one item per pass. Aneachthrough an array, or through a range whose ends are constants, makes an array as long ([int#4]); any other loop makes a slice, in the block's default region. A type written where the value goes chooses: a slice there,fixed xs: [int]@r = each …or a task's result, makes a slice in its region. A pass that fails,fail LoopResult.Manual(m)on its last line, stops the loop, which fails with it. -
Map form and
continue. Acontinueof the loop, even one that never runs, leaves its pass without an item. A slice leaves it out. An array keeps a place for every pass, so its items are maybes and that one isnone: with no type written it is[maybe int#4], and a written[int#4]is an error that asks for[maybe int#4]or a slice. -
Why it fails.
Skipped: the body never ran, its condition failing at once or itseachhaving nothing to go through (thefinallyblock does not run).Broke(maybe B): abreakstopped it, holding whatbreak = vgave, ornonefor a barebreak(thefinallyblock does not run).EndedByContinue: its final pass ended withcontinue, so the last pass did not complete normally.Manual(M): thefinallyblock's last line failed withfail LoopResult.Manual(m). Thefinallyblock fails only withfail LoopResult.Manual(…)written there; the other three say what the loop did, so a value that may fail, as another loop giving a value, takes a plan B on the last line. -
Its types.
Vis what thefinallyblock (or map-form collection) gives,Bwhat thebreaks give, andMwhat thefinallyblock fails with. Where nothing in the loop gives one, the place its value goes names it,fixed r: int ~ LoopResult<text, int> = loop …, a task's result or a parameter, and where no place does, it isbool. Thebreaks of a loop give one type, and what they and the last lines fail with is in one region. Abreakin a turn of aneachover a group gives a loop around it no value of the turn's own type, which each value makes another. On the loop's own variable,fixed r: int ~ LoopResult = loop …, a bareLoopResulttakes the loop'sBandM; anywhere else its types are written, as any kind's are. -
Its name. The variable it gives names it,
break r = v, asfixed r =,flex r =, orr =does, with a plan B after it too, and so it takes no name with=>. Where no variable is given, as in an argument,outer => loop …names it. -
Without an include. A loop gives a value in any
file. A file that writes
LoopResultitself, in a type or a variant, brings it in withinclude std/loops {LoopResult}; acase Broke bnames no kind, and needs none. -
As a statement. A loop on a line of its own with a
plan B after its
fi,each x in xs … fi { … }, is anothing ~ LoopResult: its passes give nothing, and the plan B runs when it fails. On the last line of a block that gives a value, it gives one instead. -
What it can't be. A loop that never ends by itself,
endless,loop trueor one with no condition, gives no value, since no pass of it is the final one. A loop through the values of a group, written out once per value, gives none either. Its value can't be in a region a pass declares, which is freed when the pass ends. A loop that never ends by itself takes no plan B as a statement either, which would run each time it ends. -
In C. A temporary holds the value,
Skippeduntil the loop gives one; abreakand acontinueset it, and thefinallyblock, or the gathered items when the loop ends, replace it. A map form's array is a temporary on the stack, each pass's item at its own place; its slice grows in its region as an added-to slice does.
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:
-
Every branch gives one type. The types the branches
give are joined, by the rule the items of an array follow: a branch
that gives
nonemakes the whole amaybe, and one that givesfail emakes it aT ~ E, exactly as a value becomes one anywhere else, with the wrapper outside whatever a branch already gives (see Becoming a wider type). Where every branch gives one of those, the place the value goes says what it is part of, as it does for a barenone. -
A branch may leave the task instead with
<-. It gives no value then, and the other branches settle the type. If every branch sends, nothing gives a value, which is an error. -
The last line may be a
whenor amatch, which gives the value through its own branches. That is how a chain of conditions gives one, and how a branch chooses again inside itself. -
A
matchstill covers every case, andcase _still covers the rest: a value must come from somewhere for every outcome. - A region declared inside a branch is freed when it ends, so the value can't be in it; a value in a region declared outside is fine (see Memory and Regions).
-
=belongs to a case that gives a value. The cases of amatchwritten on its own hold statements, socase p = valuethere is an error that says so.
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.
-
region rdeclares a region;region r1, r2, ...declares multiple regions on one line;new T(v) at r(ornew@r T(v)) copies a value into it. -
A pointer into
rhas type^T@r, and a type can only name regions visible where it is written. -
When the block declaring
rcloses, however it closes, everything inris 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(andr: region default) makesrthe block's default region: what allocates there needs noat 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.
-
After a
+chain,at ris the chain's."a" + label(n) at rjoins intor, andlabeltakes the block's default region, or says it needs one. A part names its own with@or in parentheses:"a" + label(n)@r at r, or"a" + (label(n) at r) at r. -
So
at rcomes last. Another+part after it, a secondat, or a field read from the value after it (label(n) at r.len) is an error, since each would read as if the region were more or less than it is;label(n)@r.lenand(label(n) at r).lengo on from the value. -
A
fail,-, ornotin front makes no memory, so the region is the call's:-count(xs) at r,<- fail why() at r. Inside anything else,2 * count(xs) at r, it names nothing and is an error; write2 * count(xs)@r. -
A plan B comes after the region:
checked(t) at r { "none" }. -
A call that can't take one says so: one whose task
takes no region last, one given its region in the parentheses
already, and
show,int(…),float(…), and a variant, which make no memory. - A call that leaves it out takes the block's default region, as every other allocation does; with none in force, it is an error that says where to name one.
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
-
What it fills in. Everything that makes memory and
would otherwise name a region:
new,new [T](…), joining text (a + b), text holding values ("{n}"), a value written as text (x.str(),text(x)),scan(prompt), and a call of a task or method that takes its region last and leaves it out (label(n)). A written region always wins, and nothing else changes: the value's type still names the region it lives in, which hover in Vela Studio shows. -
A copy is the exception.
x.copy()goes where the value it becomes goes (see Copies into a region), which is a better answer than a default, so a block's default doesn't settle one and a copy that nothing gives a region still says so. -
One per block. A second
defaultin the same block is an error: two lines that look alike would allocate in regions of different lifetimes. An inner block may name its own, which holds until that block closes; after it, the enclosing default is the one in force again. - It is opt-in. A block with no default is exactly as it always was: allocating there names a region, and the error that says so also mentions this, since making a region the default is often the shorter fix.
-
Only a region.
n: int defaultis an error, and so isdefaulton a shape's field or a variant's value. A task takes one default region at most, and a region the header only names (list: maybe ^Node@r) can't be one, since nothing can allocate there.
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
-
Tasks. A parameter of type
texttakes text that lasts. To take text of any region, a task names one in its header, as it does for pointers:name: text@rorp: Person@rtakes text inr, or text that lasts, and a result of typetext@rmay send either back. Hadgreettaken a plainPerson,greet(held)would be an error saying to name a region. -
Comparing and showing.
==compares text of any regions, andshowprints any text. -
Fields. A field or payload is written
text: its text is in the region of the value holding it, when that value names one. A shape's text fields share that one region. - C. Text is its bytes' address and how many there are, either way; the region is only in the types, checked when compiling.
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
-
Slicing.
xs[a..b]is the items fromaup to, but not including,b, without copying. It works on a slice, and on an array in a region: one a pointer points to, as inp^[0..2]forp: ^[int#4]@r, or an item of a slice. An array in a variable isn't in a region, so slicing it is an error; copy it withnew [int](xs) at r, or keep it in a region withnew [int#4](xs) at r. -
Checks. Every index and slice is checked as the
program runs (vela: slice 2..7 is out of range 0..4,
vela: slice 3..1 ends before it starts), and so is a count
for
new [T]([v#n])(vela: count -2 is negative). -
Writing. A slice's items are written through a
slice reached from a
flexname, like what a pointer points to, and never through afixedone, at any depth; itslencan't be written: it changes as items are added and taken out, or by slicing. -
Regions. A slice's items live in its region.
new [^Node](nodes) at othercopies what each pointer points to intoother, ascopy()does, sonodesmay be in any region; a slice of the values written,[p, q, ...] at other, holds the pointers as they are, so they must point intoother.
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
-
The four changes.
xs.add(v)putsvlast andxs.add(i, v)before itemi, whereimay be the length (last).xs.del(i)takes itemiout andxs.del()the last item; both give the item, which can also be dropped (xs.del(0)as a statement). The items after the change move to close or open the gap. They are built into every slice, aslenis, so no method can take their names. -
A new slice with one change.
xs.added(v)andxs.added(i, v)give a new slice withvlast or before itemi;xs.without(i)andxs.without()give one without itemior the last.xsnever changes, so they work on afixed,bindorflexname alike, afixedparameter too. The new slice is a complete copy, ascopy()makes: it shares nothing withxs, at any depth, and the value added is copied with it, so it owns nothing a fixed name owns and may go to aflexorbindname (see What a fixed name owns). It goes where a copy goes (see Copying into another region): the region of the place it is put in, or the one written after it,xs.added(v) at rorxs.added(v)@r; a block's default region doesn't settle one. An index out of range stops the program, asdel(i)'s does, and so doeswithout()on an empty slice. On a line of its own it does nothing, which is an error. Cost: each call copies the whole slice (and what its items hold), so building a list withaddedin a loop takes time growing with the square of its length, and every old copy stays in the region until the region ends; build withaddon aflexslice, and useaddedwhere the original must stay as it was. -
Where. The slice changes where it is kept, so it
must be in a place that can be assigned: a
flexvariable, a field of one, anything behind a pointer reached from aflexname (bag^.items.add(v)), or an item of such a slice. Afixed, a plain orfixedparameter, a result, a call's value, and a part taken withxs[a..b]can't change; keep them in aflexvariable first. Aflexparameter changes the task's own copy, and aflex¶meter the caller's slice (task grow(flex& xs: [int]@r, r: region), calledgrow(&xs, r)), as does a pointer to what holds it. -
The region.
addmay need more room, which it takes from the slice's region, so the task needs that region itself: aregion rit declares, or anr: regionparameter (asnewdoes).delneeds no region. -
Checks.
xs[#i]reads an item as amaybe, none out of range (see Safe index). An index out of range stops the program (vela: index 5 is out of range 0..3; foradd, the range includes the length), and so doesdel()on a slice with no items (vela: del() on an empty slice). -
Evaluation. The slice's place is found first, then
the arguments, left to right; a change inside a larger expression
happens in order with the rest (
xs.del() - xs.del()takes the last item first).
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 …
-
Reading and writing.
m[k]is the key's value, and stops the program when the map doesn't hold the key;m[#k]is the value as amaybe,nonewithout it, asxs[#i]is for an item.m[k] = vadds the key, or gives it a new value, on a line of its own. A value in a map is changed in place, as a slice's item is:m[k].x = 1, or a method that alters it.m.has(k)is whether the map holds the key,m.del(k)takes it out and gives its value as amaybe, andm.lenis how many keys it holds.m.added(k, v)andm.without(k)give a new map with the key set or taken out, a complete copy that leavesmas it was, going where a copy goes, as a slice's added and without do; a key the map doesn't hold gives an unchanged copy. -
Going through it.
each k, v in mgoes through the keys and values in the order the keys were first added (a key given a new value keeps its place), whatever their hashes, so a program writes the same thing every run._drops either.each k in m.keysandeach v in m.valuesgo through one side without making anything;m.keys at randm.values at rmake that side a slice inr, in the same order. The slice holds the keys or values themselves, so where they hold text or pointers,ris the map's own region (copy the slice to keep them elsewhere,(m.keys at r).copy() at q). As for a slice,each k, v in mgoes through the entries there when the loop started andeach k, v in &mthrough the map as it is at each pass, keys added during the loop included; a key taken out before its turn is skipped. -
Keys. A key is compared and hashed, so it is an
int, aunit, achar, abool,text(by its bytes), a pointer (by the address it holds), or a tuple of them. Afloatcan't be one, sincenanis equal to nothing, itself included. A key looked for may be text of any region; a key added lives where the map does. -
One map, several names. A map value is a handle to
its table:
fixed b = a, a field, and an argument all name the same map, so a keybadds is inatoo, anda == bsays whether they are the same map.a ~= bcompares what they hold: the same keys, each with a similar value, in any order.a.copy() at rmakes a separate map inr, with its keys and values copied too, andnew [text: int](a) at rdoes the same. Changing a map,m[k] = vandm.del(k), needs aflexplace, as a slice'sadddoes: a task that adds keys takesflex m: [text: int]@r, and since its copy of the handle names the caller's map, the keys it adds are there after the call. -
Memory. The table grows in the map's region,
doubling when more than half of its entries are in use, and a table
whose keys were mostly taken out is packed again in place, so a map
that keeps adding and taking out keys stays the same size. A
reference can't name a value in a map, and a slice can't point into
one (copy the array first,
new [int](m[k]) at r), since it moves when the map grows or is packed (see Safety). The slots a key lands in depend on a hash started from a number chosen each run, so they can't be foreseen from outside the program.
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
-
What it copies. Text, a shape or kind holding text,
pointers, or slices, a slice, a pointer (with the value it points
to), and a
maybe,T ~ E, array, or tuple of those. Copying a value that holds none of them (anint, aPointof numbers) is an error: use the value as it is. - Shared values and cycles. Two pointers to one value give two pointers to one copy, so linked data keeps its shape, and a cycle is copied as a cycle. A copied slice gets items of its own, so changing it or adding to it leaves the original alone.
-
The region. The copy allocates, so the task needs
its region as a
regionparameter or its ownregion, as fornew. A copy can't go into text that lasts (flex kept: text = line.copy()is an error): lasting text is only a literal's. -
Inside literals. A copy in a shape literal, a
variant, or an array gets its region from where the whole value
goes:
people.add(Person { name: line.copy(), age: 36 })copies into the slice's region. - Cost. Time and memory in proportion to what is copied. Text is copied with its characters, and a slice's items in one block. Pointers are followed with a list of the values still to copy rather than the C stack, so a list of a million nodes copies without running out of stack, and a table of the values already copied (freed when the copy is done) keeps shared values shared.
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)
-
One value can't be freed on its own: memory is released a whole
region at a time. For data that changes a lot over a long run, use a
region per iteration, and copy what survives into a longer-lived
region (
.copy()). -
Everything one value holds lives in one region: a shape can't hold
pointers into two different regions, or text in two, and a pointer
can't be stored in a longer-lived region (store a copy instead:
p.copy()). Text of a region can't be made to last. - A task that only names a region in its header can't allocate in it, and a region can't be returned.
-
A task can't send a pointer-free value of a type that holds pointers
(
-> Tree@rsendingLeaf(1)) unless a parameter names that region, and a name bound from a value in no region (@_, aswhen first(none) is {n}) can't be given a pointer. - Each of these has a workaround in Vela Regions, which also collects ideas for going further: easier region passing, freeing one value early, shapes with several regions, and more. The other memory models (borrowed references, move-only pointers) are in Future Directions.
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
-
Where.
includelines come first in a file, before any task, shape, or kind. A path is relative to the folder of the file that has theinclude:include geometryreadsgeometry.velanext to it,include shapes/geometryreads it from the foldershapes, and..goes up one folder (include ../common/strings). The command is the same as for one file:vela main.vela -o programreads every file the program includes, and an error in any of them names that file. The word is C's, but anincludedoesn't paste the file in: the other file stays a module with names of its own, and is read once however many files include it. Writinguseorimportinstead is an error that namesinclude. -
Names.
geometry.area(…)calls a task,geometry.Point { … }builds a shape,geometry.Square(3.0)makes a variant, andgeometry.Pointis a type anywhere a type is written (^geometry.Node@r,maybe geometry.Point,new geometry.Point(p) at r). A variant can also be named through its kind,geometry.Figure.Square(3.0). Acaseorisnames a variant alone, as incase Square side, since the value's kind already says which module it is from, or through its kind, as incase geometry.Figure.Square side. -
Bringing names in.
include shapes/palette (Color, describe)also lets the file writeColoranddescribewithoutpalette., which still works too. Each name listed must exist in the module. A kind's variants come in only when listed themselves; otherwise they are named through the kind,Color.Blue. -
Only the names listed. With braces,
include shapes/palette {Color, describe}brings inColoranddescribebut not the module's name:palettenames nothing in this file, so nothing else of the module can be reached. The parentheses include the module and the names; the braces, only the names. -
Other names.
=>gives an included module or name another name, the real name on its left:include shapes/geometry => geo, orinclude shapes/palette (Color => Hue, describe). From then on the file writes only the new name:geometry.area,Color, andpalette.Colorare errors that say what to write instead. That is how a file includes two modules whose files have the same name (include shop/prices => shopandinclude fuel/prices => fuel), or two names that would clash. A variant keeps its own name, as a field does, since it belongs to its kind; rename the kind and write its variants through it, as inHue.Red. Braces can rename names too ({Color => Hue}), but not the module, which has no name there. -
Private. Everything a file declares can be used by
the files that include it, unless it is marked
priv: a task, shape, or kind (whose variants are then private too), or a shape's field. Another file can't read or assign a private field, or give it in a literal, so a module can keep a value's invariants to itself through its tasks. (A field that others may read but only its shape's methods may change isreadinstead.) A shape with a private field can be built elsewhere only when that field names the value it falls back on, which the literal leaves out; with no such value, only the field's own module can build the shape at all. What other files see of a public task (its parameters and results), a public shape (its public fields), or a public kind (its variants' values) can't mention a private type, since they couldn't name it. -
Each module's own names. Tasks, shapes, kinds, and
variants share one namespace within a file, but each module has its
own: two modules can both have an
area, and a program can have its ownRedbesidepalette.Red. A module sees only what it declares and what it includes; the main file's names aren't visible to the modules it includes. Only the main file'smainstarts the program; a module'smainis an ordinary task, called astools.main(). The program knows a module by its path from the main file's folder, soshop/prices.velaandfuel/prices.velacan both be in one program. -
Messages. A message names things as the file with
the error writes them: expected type Hue, got int after
(Color => Hue), expected type geometry.Point, got int through the module's name. Something the file has no way to write is named by its module's path, as in expected type shapes/palette.Color, got int, and starts with./when that path reads like the name the file gives another module (./c.Thing, in a file withinclude x => c). -
Rules. Modules can't include one another in a
circle (modules can't include one another in a circle: a -> b ->
a). A file includes each module once, however its path is written
(from the folder
app,../app/shapesisshapes), and can't include two modules under one name. A name is listed once on anincludeline. The name a module is included under can't also be the name of something the file declares, of a name it brings in, or of a variable, and can't be a builtin type, task, or kind, or a variant of one. A module or name can't be renamed to itself, or to a builtin.
; 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):
- The compiler reports every mistake a program has, one after another, and at most 20 of them at a time; a mistake that stops a line from being read hides nothing else on that line, and the lexer and the parser still stop at the first thing they can't read.
- Memory is freed a whole region at a time, and a value holds pointers into one region only (see Memory and Regions).
-
Text has
==,<and the other comparisons,.len, parts (t[a..b]), joining (a + b at r), values turned into it (n.str() at r), numbers read from it (int(t)),show, andscan. It has no search, no trimming, and no case changes yet. -
Input is read a line of text at a time with
scan, or a number at a time withscan("Age: ", int); nothing reads part of a line from input; files are read and written withstd/files. -
An array's count is known before the program runs (see
Arrays). Tuples are taken apart in
isandcaseonly in parentheses, as in{(a, b)}(see Tuples). - Tasks, shapes and kinds take types, and no word asks a type for arithmetic (see Types a declaration takes).
- A task that takes types, or that sends several values, can't be a value yet, nor can a method of every value (see Tasks as values).
-
A plain lambda takes nothing from the code around it, and no lambda
can be written inside a task or method that takes types (see
Lambdas). One that keeps what it uses,
[@r], keeps copies in one region, keeps a variable as a place only where that region is declared after it (never a field or an item), isn't compared, copied, or given as a wrapper's value, and a field holds one as[@this](…),[bind @this](…), or[flex @this](…), which makes the value holding it held, never copied (see Lambdas that keep what they use). -
Only a task or a shared method takes startup decorators; a method of
every value takes only wrappers, and a task that takes types
neither, nor a task that sends several values; a builtin or a macro
is no decorator; a startup decorator sends nothing, or
nothing ~ E; a decorator's and a wrapper's values, worked out where the program starts, hold nothing in a region (see Decorators). -
A task takes one type group; a group never holds a region, which is
named beside it; a call doesn't write a group's types, a type can't
be written
(P...), and a group is given on to a task that calls itself only as it came (see Type groups). - A method's own types aren't written at a call, only told by its values or the place its value goes (see Types of its own). A method can't allocate in the region its value lives in, and is written inside its shape or kind, never in another module (see Methods and the room left to grow). A method of a shape that takes types takes no wrappers, and a method of every value no startup decorators (see Wrappers on methods).
- There are no packages to share modules between programs (see Modules).
-
showprints floats with 6 significant digits, and a value that can fail (T ~ E) has to be read first, alone or inside another value. -
A call's arguments, the parts of a join, the values in text, and an
operator's two sides are evaluated left to right whenever one of
them calls a task, has a plan B, changes a slice, or reads a line,
so
n + bump(&n)readsnbeforebumpchanges it, andshowalways works out its values left to right. Otherwise nothing they do can tell the order apart, except which of two that would stop the program stops it (see Numbers), which isn't defined. -
An expression, including a shape literal or a call's arguments, must
be written on one line, except a chain of
pipes, whose lines may start with
|>. -
A pipe gives its value to an argument, never to the value a method
is called on (
x.m()does that), and a chain's steps failing with different kinds take the kind the place names only from a binding's type or the task's when sent; given as an argument, the kinds must join by themselves. -
A number written out is at most the largest
int, so auint64past it is computed (big + big + 1) rather than written.Int512instd/int512holds a 512-bit signed integer in two's complement with full checked arithmetic. - A loop in map form takes its collection from a type written in a binding or the task's result when it is sent; given as an argument, it makes the collection its loop makes by itself. An array it makes from a range of constant ends is on the C stack, so a very long one may not fit there.
-
Statements aren't yet required to end at a line break (
show(1) show(2)on one line is accepted). -
Code may nest at most 100 levels deep (parentheses, operators in a
row, blocks inside blocks; a
new, an array literal, and a tuple count as two, and a plan B as three); deeper code is an error rather than a crash. A chain of more than about 100 operators,a + b + …, is an error that says to name part of it (fixed part = …); the arms of anelse whenchain have no limit. Text holding values may hold text holding values at most 32 deep, and a chain of modules including each other at most 100 long.
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
- Vela is intentionally small. It is not meant to replace Rust or C, but to explore how little syntax is needed for a safe, compiled systems language.
- Memory is managed with regions rather than a garbage collector or a borrow checker: lifetimes are blocks, visible in the source, and a few type rules keep every pointer valid.
- The compiler generates readable C, making it easy to debug and interoperate with existing C code.
-
Syntax uses keywords over punctuation to keep source code friendly
and self-documenting; the few symbols (
->,<-,^,@,~,&,#,;, and the comparisons~=and<>) each stand for one idea. -
Missing values and failures are types (
maybe T,T ~ E) rather than conventions such as null pointers or error codes, so the checker makes sure they are handled. -
A member of a value is always written after a dot,
.countinside a method asc.countoutside it, so nothing comes into scope unseen, and a method that changes its value says so withalters, as a parameter that changes the caller's place says so withflex&.