This document covers the surface language: syntax, semantics, modules, and the intentional omissions.
For constructor-specific rules, see constructors.md.
For namespaces, services, and standard library APIs, see services.md.
Primitive: Int, Float, String, Bool, Unit
Compound: Result<T, E>, Option<T>, List<T>, Map<K, V>, (A, B, ...), Fn(A) -> B, Fn(A) -> B ! [Effect]
User-defined sum types: type Shape → Shape.Circle(Float), Shape.Rect(Float, Float)
User-defined product types: record User → User(name = "Alice", age = 30), u.name
Unit means "no meaningful value". It is similar to void, but still a real type; diagnostics render the value as (). Effectful functions such as Console.print commonly return Unit.
All bindings are immutable. No val/var keywords — they are parse errors.
name = "Alice"
age: Int = 30
xs: List<Int> = []
Optional type annotation provides a hint to the type checker; the annotation wins over inference when both are compatible. Binding to an empty list literal without a type annotation (x = []) is a type error.
Duplicate binding of the same name in the same scope is a type error.
Arithmetic: +, -, *, / — operands must match (Int+Int, Float+Float, String+String). No implicit promotion; use Int.toFloat / Float.fromInt to convert.
Comparison: ==, !=, <, >, <=, >=.
Error propagation: expr? — unwraps Result.Ok, propagates Result.Err as a RuntimeError.
Expressions inside {} are evaluated at runtime:
greeting = "Hello, {name}! You are {age} years old."
UpperCamel callee = constructor, lowerCamel = function call. Records use named args (User(name = "A", age = 1)), variants use positional args (Shape.Circle(3.14)), zero-arg constructors are bare singletons (Option.None, Shape.Point).
All constructors are namespaced — no bare Ok/Err/Some/None:
Result.Ok(42)
Result.Err("not found")
Option.Some("hello")
Option.None
match is the only branching construct (no if/else). Patterns:
match value
42 -> "exact" // literal
_ -> "anything" // wildcard
x -> "bound to {x}" // identifier binding
[] -> "empty list" // empty list
[h, ..t] -> "head {h}, tail {t}" // list cons
Result.Ok(v) -> "success: {v}" // constructor
Result.Err(e) -> "error: {e}"
Shape.Circle(r) -> "circle r={r}"
Shape.Point -> "point"
(a, b) -> "pair: {a}, {b}" // tuple destructuring
((x, y), z) -> "nested: {x}" // nested tuple
Constructor patterns are always qualified (Result.Ok, Option.None, Shape.Circle). Records do not support positional destructuring in patterns; bind the whole record and use field access (user.name, user.age).
Nested match in match arms is supported. Arm body must follow -> on the same line — extract complex expressions into a named function.
Creates a new record with overridden fields, preserving all other fields:
updated = User.update(u, age = 31)
m = {"key" => value, "other" => 42}
=> is required inside map literals; : stays type-only.
Effects are exact method names:
fn main() -> Unit
! [Console.print, Disk.readText]
Console.print("starting")
_ = Disk.readText("data.txt")
Broad namespace declarations such as ! [Http] do not cover Http.get, and effects X = [...] aliases are no longer supported.
Programs access CLI arguments via the Args service:
fn main() -> Unit
! [Args.get, Console.print]
args = Args.get()
Console.print(args)
Run with: aver run file.av -- arg1 arg2 arg3
Arguments after -- are available as List<String>. Without --, the list is empty. Args.get() requires ! [Args.get] — argument access is visible in the signature like any other effect.
fn add(a: Int, b: Int) -> Int
a + b
fn fetchUser(id: String) -> Result<HttpResponse, String>
? "Fetches a user record from an API."
! [Http.get]
Http.get("https://api.example.com/users/{id}")
? "..."— optional prose description (part of the signature)- deeper-indented string lines continue the same description:
? "Starts the CLI." "Dispatches one argv command." aver checkwarns when non-mainfunctions omit the description! [Effect]— optional effect declaration (statically and runtime enforced)- method-level effects are supported:
Http.get,Disk.readText,Console.print - top-level functions are first-class values and can be passed where
Fn(...)is expected mainoften returnsUnit, butResult<Unit, String>is also common;aver runtreatsResult.Err(...)returned frommainas a runtime failure- function bodies use indentation
- the last expression in a function body is the return value
Regular verify blocks live directly under the function they cover:
verify add
add(0, 0) => 0
add(2, 3) => 5
Law-style verify blocks express finite universal checks over explicit domains:
verify add law commutative
given a: Int = -2..2
given b: Int = [-1, 0, 1]
add(a, b) => add(b, a)
If the identifier after law is the name of an existing pure function and the law body compares foo(args) against fooSpec(args), Aver treats that as a spec law. verify fib law fibSpec is the preferred way to say "fib should match fibSpec".
This is an intentional style choice. In Aver, the author should usually write a simple spec function and a law relating the implementation to that spec, instead of writing proof-oriented invariants directly in surface code.
verify is deterministic, not random. Regular cases run exactly as written. verify ... law ... expands the cartesian product of explicit given domains, capped at 10_000 cases.
aver check expects pure, non-trivial, non-main functions to carry a colocated verify block. Effectful flows should be tested through record/replay rather than verify.
decision blocks are first-class top-level syntax for design rationale:
decision UseResultNotExceptions
date = "2024-01-15"
reason =
"Invisible exceptions lose control flow."
"Result keeps failure explicit at the call site."
chosen = "Result"
rejected = ["Exceptions", "Nullable"]
impacts = [charge, refund, settle]
author = "team"
chosen, rejected, and impacts may reference validated symbols or quoted semantic labels. Decisions are exported through aver context ... --decisions-only.
All user-defined functions are top-level. At call time, a function sees globals + its own parameters — no closure capture at definition time.
Top-level functions are still first-class values, so higher-order builtins such as HttpServer.listenWith(port, context, handle) work without introducing lambda syntax or hidden captures.
There is no lambda syntax. List processing is typically written with recursion and pattern matching rather than callback-based helpers.
This means Fn(...) -> ... is a real type, but in practice it is mostly used for named callbacks and service handlers, not for closure-heavy functional style.
fn applyTwice(f: Fn(Int) -> Int, x: Int) -> Int
f(f(x))
fn inc(n: Int) -> Int
n + 1
Most application code in Aver stays first-order and explicit. Use function parameters when they make an API cleaner, not as a default abstraction tool.
fn sum(xs: List<Int>) -> Int
match xs
[] -> 0
[head, ..tail] -> head + sum(tail)
hasAlice = List.contains(["alice", "bob"], "alice")
ages = Map.fromList([("alice", 30), ("bob", 25)])
maybe_age = Map.get(ages, "alice")
Pure recursive functions with memo-safe arguments (scalars, records/variants of scalars) are automatically memoized at runtime. No keyword needed — the compiler detects eligibility via call-graph analysis (Tarjan SCC). Cache is capped at 4096 entries per function.
Self and mutual tail recursion is optimized automatically. A transform pass after parsing rewrites tail-position calls into a trampoline — no stack growth for recursive functions in tail position. Tail position = last expression in function body, or each arm body in a match at tail position.
This is intentionally narrower than “all recursion”. Non-tail recursion can still be expensive on large inputs, so aver check warns when a recursive function still has non-tail recursive callsites after TCO. In practice, long linear traversals are best written in accumulator style when scale matters.
Module imports resolve from a module root (--module-root, default: current working directory).
Each module file must start with module <Name> and contain exactly one module declaration.
module Payments
intent = "Processes transactions."
depends [Data.Fibonacci]
exposes [charge]
With --module-root examples:
depends [Data.Fibonacci]→examples/data/fibonacci.av, call asData.Fibonacci.fn(...)depends [Modules.Models.User]→examples/modules/models/user.av, call asModules.Models.User.fn(...)
Type errors block run, check, and verify. No partial execution. The checker covers function bodies, top-level statements, effect propagation, and duplicate binding detection.
| Absent | Reason |
|---|---|
if/else |
match is exhaustive — no silent missing cases |
for/while |
Use recursion, pattern matching, and explicit list operations |
null |
Option<T> with Some/None only |
| Exceptions | Result<T, E> only — errors are values |
| Global mutable state | No shared mutable state by design |
| Closures | All functions are top-level — no captured variables, explicit is better than implicit |
| Magic | No decorators, no implicit behaviour, no runtime reflection |