Vert Quick Reference
Vert is a small, statically typed language for games, inspired by Lua and Verse. It aims to be compact and efficient to write.
It has type inference, garbage collection, live code reloading, and ahead-of-time compilation to WASM.
Lexical
- Comments:
# to end of line. No block comments. No semicolons; statements separated by whitespace/newlines. Blocks close withend. - Numbers:
42,1_000_000,0xFF,3.14,1.5e3,12.34f64(f64 suffix on float literals only). Ints default toint, floats tofloat. - Strings:
"..."with\escapes;{expr}inside a string interpolates. - Runes:
'A','😀', escapes'\t' '\n' '\"' '\\'. - Whitespace is significant around postfix syntax and in expression lists — see the disambiguation rules under Containers.
- Other literals:
nil,true,false,{}(empty record/map/set, by context).
Declarations
let x = 42 # inferred
let x: float = 1.0 # annotated
let x: int # declared, assigned later
x := 42 # shorthand for let x = 42
const limit = 100 # immutable (deep: no mutation through const)
let a, b: float, c = (1, 2.0, "s") # tuple destructure (per-name annotations ok)
type Vector2 = {x: float, y: float} # type alias
Reassignment is plain = (no let). Tuple destructuring is available on declarations: let a, b = (1, 2).
Compound assign += -= *= /= %= works on variables, arr[i], and obj.field.
Enum and function-value declarations require an initializer; use an optional
type when absence is part of the value.
Types are inferred everywhere an annotation is omitted (locals, returns,
generics); annotate to constrain or document.
Types
| syntax | meaning |
|---|---|
int int64 float float64 bool byte rune string void |
primitives (float is 32-bit, int 32-bit; *64 widen) |
T? |
optional |
T[] |
array |
T[N] |
fixed-size inline value array; N is a nonnegative integer literal or integer-literal generic parameter |
map[K V] / map[K, V] |
map |
set[T] |
set |
() / (A,) / (A, B, ...) |
tuple (zero or more elements; the comma distinguishes a 1-tuple) |
{f: T, g: U} |
structural record |
task[T] |
spawned-task handle |
Name / Name[A, B] |
class/interface/enum/alias, optionally generic |
Operators
- Arithmetic:
+ - * / %, unary-.+also concatenates strings, arrays, maps (right wins duplicate keys), and unions sets;-on sets is difference. - Comparison:
== != < > <= >=, chainable (0 <= i < n). Records/arrays/ tuples/maps/sets/enums compare structurally;uniqueclass instances by identity. - Logic:
and or not. On failable operands,orpicks the first success. - Postfix
?: asserts/propagates a failable or optional (see Failure). ?.: safe field access on optionals —point?.xyieldsint?.- Ranges:
a..binclusive;a..<bexcludes the end;a..>bexcludes the end of a descending range; append..stepfor a stride (1..10..2,5..1..-1). Bounds may be int/int64/float/float64/byte/rune (start decides; step is int for byte/rune ranges, never zero). A range is an ordinary 2/3-tuple value — storable and passable, iterable infor. - Precedence:
or<and< comparisons <..<+ -<* / %<not/unary < postfix.
Functions
func add(a: int b: int): int return a + b end
func log(msg: string, level = 1, color = "white"): string ... end
log("hi" level: 2) # named args skip/reorder defaults
func inner_demo(): int
func inner(): int return 1 end # nested funcs close over locals
let f = inner # functions are first-class values
return f()
end
- Signature:
func name(params) <effect>...: ret_type body end. Return type and effects are optional (inferred).returnwith no value only beforeend/else/elseif. - Multiple returns:
return a, byields a tuple. - Params:
name: type,name: type = default,name = default, barename. Commas between parameters and call arguments are optional. - Function types use the same signature shape:
func(int string)<reads>: bool. - Brackets remain neutral until type checking:
identity[int](value)applies an explicit generic argument whenidentityis polymorphic, whilefunctions["double"](value)indexes an indexable value and calls the resulting function. - Short lambdas are
x => expr,x: int => expr,(x y) => expr, and() => expr. Add effects and a result type after a parenthesized parameter list, as in(x: int)<reads>: int => expr; use=> do ... endfor a multi-statement body.
Generics
Generic declarations use source-ordered parameters: [T] is a type,
[T: Named] constrains a type, [N: int] is a compile-time integer literal,
and a final [Ts...] or [Ts...: Named] is a type pack. Explicit application
may be partial (pair[int](1, "x") infers the remaining type), and arguments
may use commas or whitespace. A trailing symbolic pack spreads as f[Ts...].
type Pair[T] = (T, T) # transparent generic alias
func same[T, U where T == U](a: T, b: U): Pair[T] return (a, b) end
func width[N: int](): int return N end # width[8]() folds to 8
func columns[Ts...](): (for T in Ts do T[] end)
return for T in Ts do [] end
end
where also accepts structural call/operator requirements. Constraints and
witnesses are checked at compile time and erased by specialization. A return
type some Interface or some iterable[T] hides one stable concrete result
type while exposing only that constraint; some is legal only as the entire
function return type.
A pack is eliminated by for: (for T in Ts do F[T] end) is a tuple type,
and a marked term comprehension produces a tuple value. One expansion may be
spliced among fixed tuple elements, including both a prefix and suffix:
(id, for x in values do x end, tail). Pack comprehensions accept exactly one
source and no filters. They are compile-time expansion syntax, not runtime
iteration over arbitrary tuples; an ordinary for comprehension still
produces an array. A contextually known empty pack yields (), while an
unconstrained empty call is a cannot infer pack error.
Control flow
Everything is an expression: if, case, loops, and do blocks yield values
(a loop yields the array of its body values).
if cond then ... elseif cond2 then ... else ... end
let r = if ok? then 1 else 2 end
if v := lookup(), v > 0 then print(v) end # header clauses: comma-separated
# conditions and := bindings; each
# must succeed for the body to run
while cond do ... end # same header-clause form
loop ... break ... end # infinite until break
do ... last_expr end # block expression → last value
for x in values do ... end # arrays/sets: one binding
for i in 0..3 do ... end # inclusive range
for x in 5..>1..-1 do ... end # exclusive bound + step
for c in 'a'..<'d' do ... end # rune (and float, ...) ranges
for k, v in map do ... end # maps: key + value bindings
for b in "text" do ... end # strings iterate as bytes
for x, y, z in triples do ... end # tuple elements destructure
# positionally, any arity;
# commas optional: for x y z in ...
for v in 1..10, v != 3, v != 7 do ... end # filter clauses
for a in xs, b in ys do ... end # nested (cross product)
let doubled = for v in 1..5 do v * 2 end # comprehension
The final example is an ordinary runtime comprehension and yields an array.
Only a for classified from a declared pack-expansion type is a compile-time
pack comprehension; it yields a tuple and cannot have filters or extra sources.
User-defined iteration uses a failable first/next value-cursor protocol.
Failure means exhaustion; each success returns the continuation cursor and
the current element. Arrays, strings, sets, and maps expose the same functions
as built-ins; map elements are (key, value) tuples and strings yield bytes.
func first(c: Countdown) <decides>: (int, int)
c.from > 0
return (c.from, c.from)
end
func next(c: Countdown, cursor: int) <decides>: (int, int)
cursor > 1
return (cursor - 1, cursor - 1)
end
Places, mutable views, and inout
Ordinary iteration binds mutable local copies; assigning the binding does not change the collection. A place view instead yields scoped assignable storage:
let xs = [1 2 3]
for x in xs do x += 10 end # xs is still [1 2 3]
for x in xs.places() do x += 10 end # xs is [11 12 13]
let scores: map[string int] = {"a" = 1}
for key, score in scores.value_places() do score += 1 end
A place store happens immediately and only on an executed assignment. Reading
the binding as a value, passing it to a value parameter, or let copy = place
materializes a copy. A place cannot be returned as a place, stored, captured,
or carried across suspension. inout preserves it for one direct,
non-suspending call:
func bump(value: inout int): void value += 1 return end
for x in xs.places() do bump(inout x) end
Builtin provider decorators compose statically and disappear during lowering.
tracked(changed, epoch) stamps the parallel int64[] only when a store
executes; its optional third argument is a direct notifier call, evaluated
after each executed store, and may use the contextual provider_row index.
changed_since(changed, consumer_epoch) filters using explicit, independent
consumer epochs. transactional() states that all provider barriers
participate in ordinary failure rollback (as they do when nested in a failable
transaction).
For a flat soa[T], indexed(barrier) selects an indexed store kernel outside
the row loop. barrier must be a direct function accepting the row followed by
the flattened old fields and flattened new fields. Its result is ignored. It
updates application-owned buckets without a runtime callback or provider
dictionary; an unindexed view contains no barrier call.
User-defined view declarations statically zip value and place sources. They
stop at the shortest source and do not construct an iterator tuple or a
runtime provider object:
view moving(ids: int[], positions: soa[Pos], velocities: soa[Vel],
changed: int64[], epoch: int64)
value ids
place positions.places().tracked(changed, epoch)
place velocities.places()
end
for entity, position, velocity in moving(ids, positions, velocities,
changed, epoch) do
position.x += velocity.x
end
Optional enter, exhaust, and leave expressions delimit a scoped lease.
exhaust runs only after full natural exhaustion; leave also runs on
break, return, failure, and cancellation unwinding. Hooks cannot fail or
suspend. Every view argument and source is acquired once.
A view may replace its direct yields with a runtime segment clause. Each segment supplies the same static yield shape; the segments concatenate under one lease without materializing rows:
view rows(q: Query)
enter acquire(q)
segment chunk in q.chunks do
value chunk.ids
place chunk.positions.places()
end
exhaust consumed(q)
leave release(q)
end
A public, one-parameter view named places is the implicit compile-time
place-iteration protocol for a matching non-builtin value, so for ... in q
can use the view without a runtime provider object.
A trailing N14f type pack can expand one access mode across a tuple of stable sources. The tuple is compile-time shape information, not a tuple of places:
view packed[Ts...](ids: int[], columns: (for T in Ts do T[] end))
value ids
place columns...
end
for entity, position, velocity in packed[Pos Vel](ids, (positions, velocities)) do
position.x += velocity.x
end
case is the pattern match. Subjects are enums (exhaustive) or runes;
commas between arms are optional:
let label = case dir
Direction.north => "N",
Direction.south => "S",
_ => "other" # wildcard; otherwise all variants required
end
case r
Result.ok(value) => value, # bind payload fields positionally
e := Result.err => do print(e.message) 0 end, # bind whole matched value
end
let n = case ch 'a' => 1, _ => 2 end # rune subject with literal patterns
Failure (<decides>) and effects
A <decides> function either returns or fails. Failure is transactional:
writes made before fail roll back.
func validate(age: int) <decides>: int
if 0 <= age <= 150 then return age else fail end
end
let v = option{validate(10)} # int? — value on success, nil on fail
let ok = bool{validate(10)?} # true/false
let n = validate(10)? + 1 # ? propagates failure to enclosing context
let w = primary() or backup() # first success wins
if validate(x) then ... end # if/while headers are failure contexts
Effect annotations, <name> after the param list (multiple allowed,
all inferred when omitted; annotations are checked against inference):
<succeeds>— cannot fail (default).<decides>— may fail.<suspends>— may suspend; mutually exclusive with<decides>.<converges>— terminates (default);<computes>— pure but may diverge.<reads>/<writes>— reads/writes shared state (module bindings, captured mutables, class fields). Local mutation stays pure.<allocates>— creates managed refs (class instances, closures, spawn).<transacts>— shorthand for reads+writes+allocates+diverges.<no_rollback>— opts out of failure rollback.
Optionals
let v: int? = option{42}
let none: int? = nil
let sum = v? + 2 # ? unwraps (fails if nil)
let has = bool{v?}
let x = point?.x # safe access → optional
Containers
let a = [1 2 3] # array; [] is empty; commas are optional
let s = a.slice(1, 3) # a.length(), push, etc. — see /std + methods
let fixed: int[3] = [1 2 3] # contextual fixed array; length is part of its type
let zeros = int[4]{0} # fill every fixed slot
let checked = option{int[3]{a}} # dynamic → fixed checks length and may fail
let dynamic = array{fixed} # fixed → dynamic makes a fresh copy
let m: map[string int] = {"Ada" = 10 "Bea" = 20}
m["Ada"] = 30 m.delete("Bea") m.length()
let k: map[Enum int] = {E.a = 1, node = 2} # map keys are arbitrary exprs
# (strings, enums, unique class instances, ...)
let st: set[int] = {1, 2, 2} # dedupes
let st2 = {1 2 3} # unannotated {e e ...} is a set
let hit = bool{st[2]} # set/map indexing is failable
let t = (1 2.0 "three") # tuple; (x,) is a 1-tuple
let r = {x: 0.0 y: 0.0} # record literal: identifier keys
type P = {x: int, y: int}
let p: P = {3 4} # contextual positional record initializer
Braces disambiguate by content: {} empty, {f: e} record, {e = e} map,
{e, e} / {e e} set — except {e e} against an expected record type is a
positional record initializer (values in declaration order). Record keys
cannot be strings or expressions (use a map).
Whitespace is significant in expression lists. Arrays, tuples, calls,
records, maps, sets, case arms, parameters, patterns, enum variants, and type
lists accept commas or whitespace between items, but one list cannot mix the
two styles: (x, y z) is invalid. Disambiguation rules:
- Postfix must attach to its operand:
f(x)calls,a[i]indexes,P{x: 1}constructs — whilef (x),a [i],P {x: 1}are two separate expressions in a list context. - A space before
.,?., or postfix?is a parse error (a .f,r ?.x,v ?); space after the dot is fine (a. f). - Infix vs prefix by spacing:
x - yandx-ysubtract;x -yisxthen-yas two list items.
Indexing a[i] / m[k] is failable (makes the function <decides> unless
handled). Iteration order and all semantics are deterministic.
Concurrency (<suspends>)
Scheduling is deterministic (wake order = task creation order).
sleep(seconds) suspends (sleep(0) yields).
Structured forms — child tasks cannot escape the enclosing lexical scope; any still running when the scope exits are cancelled:
sync a() b() end # run arms concurrently, wait for all → tuple of results
race slow() fast() end # first result wins; losers are cancelled immediately
rush a() b() end # first result wins; losers keep running in the
# enclosing scope until it exits
branch announce() end # fire-and-continue: no result, caller doesn't block,
# child is still scoped to the caller
A task[T] expression used as an arm is adopted instead of called. The form
owns and drives that existing task; a task may be adopted only once. A bare
branch h adopts task handle h into the current lexical scope.
Unstructured — the task outlives its creating scope:
let h: task[int] = spawn work() # typed handle, escapes scope
h.await() h.cancel() # cancellation unwinds the child's defers
let g: task[int] = hold work() # held: runs only when explicitly stepped
g.step() # Stepped.ran, .blocked, or .done
g.done() g.release() # observe completion or return to scheduling
h.hold() # remove a scheduled task from rotation
yield() is a held-task step boundary. In scheduled work it behaves like
sleep(0); within one held step, sleep(0) settles immediately while
yield() parks until the next step.
defer expr runs at lexical scope exit, LIFO; discarded if a speculative
context fails. sync/race/rush/sleep/yield require <suspends>;
spawn/branch don't propagate it to the caller.
Enums
enum Status active, inactive end # commas optional
enum Result ok{value: int} err{message: string} end # payload variants
let s: Status = Status.active # always qualified
let r = Result.ok{value: 1}
Closed, no bare variants, ==/!= comparable, usable as map/set keys.
Classes and interfaces
class Character
name: string # field
health: int = 100 # field with default
speed := 1.0 # := infers field type
const max_health: int = 100
func take_damage(amount: int): void
Self.health = Self.health - amount # Self required for members
return
end
end
let hero = Character{name: "A"} # construction: named fields;
# defaulted fields optional
abstract class Entity # modifiers: abstract | final | unique
abstract func describe(): string # abstract method: no body
end
class Player extends Entity
override func describe(): string return "p" end # override required;
end # `final func` / `override final func` seal
unique class Node value: int end # identity equality; usable as map/set key
# (non-unique classes are rejected as keys)
class Box[T] value: T end # generics; constraint: [T: Interface]
let b = Box[int]{value: 1} # explicit args (often inferred)
class IntBox extends Box[int] end
interface Named
name: string
func label(): string
end
interface Fancy extends Named end # interface inheritance (multi via ,)
class Item implements Named ... end # implements list via ,
let n = Named{item} # dynamic cast to class/interface
super calls the base implementation. Class instances are reference values
(aliases share mutations); records/tuples are values.
Conversions and captures
int{x} int64{x} float{x} float64{x} # numeric conversions
byte{65} byte{'A'} # from int or rune
string{42} string{'😀'} string{bytes} # primitives/rune/array[byte] → string
array{"text"} # string → byte[]
array{fixed} # fixed array → fresh dynamic array
T[N]{value} / T[N]{dynamic} # fill / checked dynamic → fixed
bool{failable?} option{failable} # capture failure as bool / optional
Name{expr} # dynamic cast (classes/interfaces)
Builtins and modules
print(a, b, ...) (tab-separated), sleep(seconds).
import /std/math as math # math.sqrt, ...
import /std/text as text # text.get, text.insert, ...
import Vec3 Mat3 from /std/la # commas may replace all spaces
import * from /std/la # all public values and types
Only absolute paths and /std/* modules; no relative imports. A module alias
is a value (let m = math) but has no declarable type, so it can't pass
through function parameters. Declarations are private by default; prefix a
top-level declaration with pub to export it. Namespace aliases qualify both
values (la.cross) and types (la.Vec3). Selective imports use either all
spaces or all commas, never a mixture. pub import Name from /path selectively
re-exports a dependency; namespace and wildcard re-exports are unavailable.