The Velaris language reference
This is the specification: what Velaris means, precisely. It is not a
tutorial ([TUTORIAL.md](TUTORIAL.md) is), and it does not teach
programming. It exists so that anyone deciding whether to depend on
this language can find out exactly what it promises — and what it
does not.
Version 2.30. Where this document and the implementation disagree,
that is a bug in one of them; please report it.
1. Programs
A program is one or more files of UTF-8 text with the extension
.vel. Execution begins at main, which takes no parameters and
cannot fail. A program without main is rejected (E400).
Files are combined by import (§10). There is no separate linking
step and no build configuration: the entry file plus what it imports
is the program.
2. Lexical structure
Comments run from // to end of line. Whitespace is insignificant
except as a separator; there is no layout rule and no significant
indentation.
Identifiers begin with a letter or underscore and continue with
letters, digits or underscores. They are case-sensitive.
Keywords: `fn let return if else uses true false while for requires
ensures and or not invariant record import fail check try`.
Literals:
| Kind | Examples | Notes |
|---|---|---|
| Int | 0, 42, -7 | 64-bit, signed (§4.1) |
| Float | 1.5, 0.0, -2.25 | IEEE-754 binary64 (§4.2) |
| Bool | true, false | |
| Text | "hello", "a\nb" | escapes: \n \t \\ \" \r \0 |
| List | [1, 2, 3] | all elements one type |
| Map | {"a": 1} | keys Text or Int |
An empty [] or {} has no inferable element type; give it one with
a typed let (E506, E507).
3. Types
Int Float Bool Text Handle
List of T
Map of K to V (K is Text or Int)
fn(T, ...) -> R (a function value; pure only)
<record name>
There is no null, no undefined, no implicit conversion, and no
subtyping. A value has exactly one type, known at compile time.
Handle is an opaque reference to a value living in the host language
(§12). It can be passed and stored; it has no operations of its own.
Generic functions are written for any T and are instantiated at each
call site by unification with the argument types. There are no
constraints or bounds.
4. Numbers
4.1 Whole numbers
Int is a signed 64-bit integer: −9223372036854775808 to
9223372036854775807. Arithmetic that leaves that range is an error
(E407), not a wraparound and not a promotion to a larger type. This
holds identically in interpreted and natively compiled code; the two
are checked against each other by a fuzzer on every release.
/ on two Ints is division that rounds toward negative infinity
(-7 / 2 is -4). % returns the remainder with the sign of the
divisor, so x == (x / y) * y + (x % y) holds for all y != 0.
Division or remainder by zero is an error (E403), never an infinity.
4.2 Decimals
Float is IEEE-754 binary64 with round-to-nearest-even, including
signed zeros, infinities and NaN. Float division by zero follows
IEEE-754 and yields an infinity; this is the one place the language
does not raise an error, because it is what the hardware defines.
Int and Float never mix implicitly. to_float(x) widens;
round(x) narrows.
4.3 Text
Text is a sequence of Unicode code points. length counts code
points, not bytes, and code_at(t, i) returns the code point at a
position. Comparison (<, >, <=, >=) is lexicographic by code
point. Text is immutable; + produces a new value.
5. Values and mutation
Records, lists and maps are immutable. push, put and record
construction produce new values; nothing observes a change made
elsewhere. let introduces a binding; assignment (x = e) rebinds a
local name and never mutates a value another name refers to.
Equality (==) is structural for records, lists and maps, fpEQ for
Float (so NaN is not equal to itself, and 0.0 == -0.0), and
ordinary equality elsewhere.
6. Evaluation
Evaluation is strict, left to right, depth first. Arguments are fully
evaluated before a call. and and or short-circuit: the right side
is not evaluated when the left decides the result. if/while
conditions must be Bool; there is no truthiness.
There is no undefined behaviour. Every operation either produces a
value, raises a language error with a code, or fails in the sense of
§8.
7. Effects
A function declares what it may do:
fn save(path: Text, body: Text) uses fs { ... }
The effects are io (console), fs (files), net (network),
clock (the time), rand (randomness) and ffi (calling the host
language, §12).
The rule is transitive and checked at compile time: a function may
only perform effects it declares, and calling a function requires
declaring everything that function declares. A function with no uses
clause is pure — it cannot perform any effect, and neither can
anything it calls, however deep. Violations are E300.
This is a property of the whole call graph, not a convention. Reading
a signature tells you the complete set of things a call can do to the
outside world.
8. Failure
A function that can fail says so:
fn parse(t: Text) -> Int or fail { ... }
Inside it, fail "reason" stops that call. A caller must handle the
possibility, in one of two ways:
check parse(t) { ok n { ... } fail why { ... } } // handle here
let n = try parse(t) // pass it up
try is only allowed inside a function that itself says or fail.
Ignoring a fallible call is a compile error (E520). main cannot
fail.
Fallible builtins: to_int, read_file, fetch, post,
fetch_status, get on a map, the py_* family, and the json_*
readers. get on a list is not fallible: list bounds are the
prover's domain (§9.4), and get_or(m, k, default) gives a total map
lookup.
9. Contracts and proof
9.1 What you write
fn f(x: Int) -> Int
requires x >= 0 // what the caller must ensure
ensures result >= x // what f guarantees in return
{ ... }
while i < n
invariant total >= 0 // true before and after each turn
{ ... }
Contract expressions must be pure and may call pure functions.
result names the return value in ensures.
9.2 What "proven" means
When Velaris says a promise is proven, it means: for every input
permitted by the requires, the ensures holds — established by the
Z3 theorem prover before the program runs, using the semantics in this
document, with no execution and no sampling.
When it says a promise cannot be kept (E700), it means a
counterexample exists and is shown. This is only reported when the
counterexample involves no summarized calls, so a reported violation is
always literally realisable.
When neither can be established, the promise is checked at runtime
instead, and violating it is an error when it happens (E600, E601).
velaris explain reports which of the three applies to each function.
The compiler never reports a promise as proven when it was in fact
left to a runtime check.
9.3 What is proven, and what is not
Proven today: whole-number and boolean arithmetic; comparisons; loops
(with written invariants, and with inferred bounds on counters);
records, including fields that are lists, floats or text; flat lists
and lists of lists via the theory of arrays; maps, modelled as values
plus which keys are present; quantified list properties through
all_of / any_of; failure paths, so ensures applies to every path
that returns; division and remainder, including that the divisor is
never zero; Float in genuine IEEE-754 rather than as real numbers;
length and contains on text, upper/lower as length-preserving,
and split as producing at least one piece.
Not proven, and checked at runtime instead: the contents of text
beyond the above; anything involving values that come back from the
host language; and any obligation the solver cannot settle within its
budget. Loop invariants are inferred only for simple counter bounds;
anything richer must be written.
9.4 Calls, and the soundness rule
Calls are proven modularly: at a call site the callee's ensures
is assumed and its requires becomes an obligation on the caller
(E701). A callee's body is never inlined into a caller's proof.
If any premise cannot be translated into the solver's logic, the whole
proof for that function is abandoned and its promises fall back to
runtime checks. Proving with a dropped premise could manufacture a
counterexample that is not real, so it is never done.
10. Modules
import "std.vel" // names merge into this file
import "lib/geo.vel" as geo // names live behind geo.
A plain import merges the imported file's functions and records, with
duplicate names rejected. A named import prefixes that file's
functions; the library's internal references are rewritten with it, so
a library behaves identically from the inside. A local name may not
shadow an import name (E514).
Imports are resolved relative to the importing file, with the bundled
standard library searched last. Import cycles are rejected.
11. Compilation and execution
A program is lexed, parsed, effect-checked, type-checked,
proof-checked, then run. Pure functions over Int, Float, Bool,
list reads and text — and, since 2.14, functions whose contracts are
proven — may be compiled to machine code through LLVM. Everything
else is interpreted.
Native and interpreted execution are required to produce identical
results. Where they cannot be made identical, the operation is not
compiled: / and % stay interpreted so that division by zero is a
clean error in both, and results of type Text are not returned from
native code because that boundary is platform-specific. A fuzzer
generates random programs and compares both engines on every release.
If the native backend is unavailable or fails for any reason, the
program runs interpreted with the same behaviour.
12. The host language
py(module, function, args) -> Text
py_int / py_float -> Int / Float
py_json(module, function, args) -> Text (JSON in, JSON out)
py_new(module, function, args) -> Handle
py_do(handle, method, args) -> Text
py_field(handle, name) -> Text
py_close(handle)
All of these require uses ffi and all except py_close can fail.
Arguments travel as a JSON list; a trailing JSON object becomes
keyword arguments. A value the host returns that is not JSON comes
back as a Handle.
Nothing about values crossing this boundary is verified: the prover
treats them as unknown. What the language still guarantees is that
crossing it is visible — a function that reaches the host says
uses ffi, and a pure function cannot.
13. Concurrency
**Velaris is single-threaded, deliberately, and has no concurrency
model.** There are no threads, no async functions, no channels, and no
parallel execution. A program is one sequence of steps.
This is a position, not an oversight. The language's central claim is
that a signature tells you what a function can do; concurrency
introduces effects — data races, interleaving, deadlock — that a
signature of the current design cannot express. Adding threads without
extending the effect system to describe them would break the one
promise the language exists to make.
If concurrency is added, it will be as an effect with rules stated
here first. Until then, a Velaris program that needs parallelism should
get it outside the program: run several, or reach the host language
through uses ffi and accept that what happens there is unverified.
14. Errors
Every error has a stable code (E###), a message in plain English, a
file and line, and numbered suggested fixes. --json emits them as
structured data. The complete list is generated from the compiler
source itself and published with the documentation.
Codes are grouped: E0xx lexing, E1xx parsing, E2xx names, E3xx
effects, E4xx arity and runtime arithmetic, E5xx types, E6xx runtime
contract violations, E7xx proof results.
15. Versioning and stability
Velaris follows semantic versioning. Breaking changes happen only at
major versions; 2.0 made four builtins fallible and the compiler
pointed at every call site that needed updating. Minor versions add;
patch versions fix.
The test suite runs on Linux, Windows and macOS, on two Python
versions, with and without the optional solver and native backend, on
every push.
16. What this language does not have
Stated plainly, because a specification that only lists strengths is
advertising: no threads or async (§13); no exceptions — failure is in
the signature (§8); no traits, interfaces, classes or inheritance; no
closures — function values cannot capture their surroundings; no
mutable data structures; no reflection; no macros; no operator
overloading; no package registry (libraries are vendored, §10); no
incremental compilation beyond proof caching; and a compiler written
in Python, which is clear to read and slower than a production
compiler.