Language specification
This document specifies the Yona language as implemented by the reference
compiler yonac. Normative rules are stated in prose; implementation notes
describe how yonac realizes them and are informative, not binding on other
implementations.
A Yona program is a single expression. Compiling a source file that contains an expression produces an executable whose exit code is the expression’s value (for integer results). A source file may instead contain a module declaration, which compiles to a linkable object file plus an interface file (§7).
1. Lexical structure
Section titled “1. Lexical structure”1.1 Source text
Section titled “1.1 Source text”Source files are UTF-8. Identifiers are ASCII: functions and variables match
[a-z_][A-Za-z0-9_]* (camelCase by convention), type and constructor names and
module segments match [A-Z][A-Za-z0-9_]* (PascalCase).
1.2 Comments
Section titled “1.2 Comments”# a line comment runs to end of line## a doc comment — extracted by the API documentation generator
/* a block comment /* block comments nest */ and may span lines */# introduces a line comment, including a leading #!… shebang line. ## at
the start of a line is a documentation comment, attached to the following
declaration by documentation tooling; to the compiler it is an ordinary comment.
/* … */ comments nest and may contain newlines.
1.3 Newlines
Section titled “1.3 Newlines”Newlines are significant tokens. A newline (or a ;, which is equivalent)
terminates an expression in the three positions where expression sequences
occur: case arms, do-block steps, and module-level function
bodies.
A newline is suppressed — treated as ordinary whitespace — in exactly these situations:
- Inside brackets
(),[],{}. Bracketed expressions may span any number of lines. Exception: when acase,do,with, orhandleblock is open inside the brackets, newlines again act as clause separators, so the block’s arms still terminate correctly. - After a binary operator or continuation token (
+,*,->,=,,,|>, …). This permits natural line continuation:
let total = price + taxin totalThis rule is what allows juxtaposition application (§3.6) to coexist with
expression sequences: f x y never runs onto the next line accidentally,
because the newline ends it unless an operator invites continuation.
1.4 Keywords
Section titled “1.4 Keywords”let in do end case of if then else with asmodule import export from type trait instancetry catch raise extern async daemonperform handle resume effect for1.5 Literals
Section titled “1.5 Literals”| Form | Examples | Notes |
|---|---|---|
| Integer | 42, -17, 1_000_000 |
64-bit signed; _ separators permitted between digits |
| Float | 3.14, -0.5, 1.23e-4 |
IEEE 754 double |
| String | "hello", "a\nb" |
UTF-8; escapes \" \\ \n \r \t \0; interpolation §3.2 |
| Character | 'a', '\n' |
single Unicode scalar |
| Boolean | true, false |
|
| Unit | () |
the empty tuple; the type and value of “nothing” |
| Symbol | :ok, :not_found |
interned atoms, snake_case by convention |
Implementation note. Symbols are interned to 64-bit integer identifiers at compile time; symbol comparison is a single integer comparison, and matching on symbols compiles to an integer switch.
2. Values and their syntax
Section titled “2. Values and their syntax”2.1 Collections
Section titled “2.1 Collections”[1, 2, 3] # sequence (persistent list)[] # empty sequence(1, "two", true) # tuple — fixed arity, heterogeneous(42,) # one-element tuple{1, 2, 3} # set{"name": "Ada", "age": 36} # dictionary{} # empty dictionarySequences, sets, and dictionaries are persistent: every operation returns a new value sharing structure with the old one. Tuples are fixed-arity product values.
Implementation note. Small sequences are flat arrays; large ones are radix-balanced tries. Dictionaries and sets are hash array mapped tries (HAMTs). All share structure on update. See Persistent data structures.
2.2 Generators (comprehensions)
Section titled “2.2 Generators (comprehensions)”[x * 2 for x = xs] # sequence generator[x for x = xs, if x > 3] # with guard{x * 2 for x = xs} # set generator{k : v * 10 for k = ks} # dictionary generator[| f x for x = xs ] # parallel generator (§6.3)The general form is [expr for pattern = source] with an optional , if guard.
The source is any sequence-valued expression.
Implementation note. Generators compile to counted loops, not closures. With a guard, a two-pass strategy first counts matches, then fills the result without reallocation. Chained collection pipelines are stream-fused into a single loop when the compiler can prove it safe.
3. Expressions
Section titled “3. Expressions”3.1 let
Section titled “3.1 let”let x = 42 in x + 1 # single bindinglet x = 10, y = 20 in x + y # multiple bindingslet add x y = x + y in add 3 4 # function-definition bindinglet (a, b) = (1, 2) in a + b # pattern bindinglet _ = println "side effect" in 42 # discard bindinglet bindings in body introduces bindings scoped to body. Bindings are
separated by commas. A binding’s left side is a pattern; a name followed by
parameter patterns is sugar for binding a lambda. Nested let is allowed
(let y = let z = 1 in z * 2 in y); style still prefers one multi-binding let
when the bindings are independent. A type annotation may precede a function
binding on its own line:
let add : Int -> Int -> Int add x y = x + yin add 3 4Evaluation order. Bindings that depend on earlier bindings observe their
values. Bindings that are independent of one another have no defined
sequential order and may be evaluated concurrently (§6.2). Code whose side
effects require an order must use do.
3.2 Strings and interpolation
Section titled “3.2 Strings and interpolation”Within a string literal, {name} interpolates a variable and {(expr)}
interpolates a parenthesized expression; non-string values are converted to
their textual form. An empty pair, {}, remains literal so it can be used by
placeholder-based APIs such as Std\Format.format. Write {{ or }} for a
literal opening or closing brace next to interpolation:
let x = 6 in "the answer is {(x * 7)}" # "the answer is 42"let x = 42 in "{{{x}}}" # "{42}"3.3 do
Section titled “3.3 do”do fd = tcpConnect "localhost" 8080 # binding step send fd "hello" # expression step response = recv fd 4096 response # value of the blockenddo … end evaluates its steps strictly in textual order. A step of the form
name = expr binds name for subsequent steps. The block’s value is its last
expression. do is the sequencing primitive; use it whenever side effects must
happen in order.
3.4 if
Section titled “3.4 if”if x > 0 then "positive"else if x < 0 then "negative"else "zero"if is an expression; both branches are required and must have the same type.
3.5 Functions and lambdas
Section titled “3.5 Functions and lambdas”add x y = x + y # space-separated parameter patternsfactorial n = case n of 0 -> 1 _ -> n * factorial (n - 1)end
abs x if x >= 0 = x # guard; next clause if falseabs x if x < 0 = -x
scale : Float -> Float -> Float # optional annotationscale factor x = factor * x
\x -> x * 2 # lambda\(x, y) -> x + y # lambda with tuple pattern\-> expensive () # zero-argument lambda (thunk)There is no name(x, y) -> body definition form. name (x, y) = body is a
single tuple pattern, not two parameters.
A function of several clauses is matched top to bottom; the first clause whose patterns (and guard, if present) match is selected. Functions are first-class values; partial application is automatic:
let add5 = add 5 in add5 10 # => 15Zero-arity functions auto-evaluate when referenced by name (Yona is strict).
To pass one as a value, wrap it in a thunk: \-> f.
3.6 Application
Section titled “3.6 Application”Application is by juxtaposition — f x y. f(x) is the same as f x.
f(x, y) applies f to the tuple (x, y); it is not a two-argument call.
Juxtaposition binds tighter than every binary operator: f x + g y parses as
(f x) + (g y).
Pipes reverse application order for pipeline style:
value |> stage1 |> stage2 # stage2 (stage1 value)stage2 <| stage1 <| value # the same, right-to-leftImplementation note. yonac compiles functions by deferred
monomorphization: a definition is stored as a typed AST and compiled at each
call site where concrete argument types are known. Closures capture free
variables in a heap environment; a closure value is
{fn_ptr, ret_tag, arity, captures…}.
3.7 case
Section titled “3.7 case”case value of 0 -> "zero" n if n > 0 -> "positive" _ -> "negative"endcase scrutinee of clauses end evaluates the scrutinee once, then tests clauses
top to bottom (§4 defines patterns). The first matching clause’s body is the
expression’s value. All clause bodies must have the same type. If no clause
matches at runtime, the program aborts with a match error; the compiler warns
when it can prove a constructor uncovered.
3.8 with (resources)
Section titled “3.8 with (resources)”with handle = tcpConnect "localhost" 8080 in send handle "hello"# handle is closed when the body completeswith name = resource in body evaluates resource, binds it to name,
evaluates body, and then releases the resource by calling the Closeable
trait’s close method — resolved statically for the resource’s type. Using a
value whose type does not implement Closeable is a compile-time error.
Current limitation. Release is guaranteed when body completes normally. If
an exception propagates out of body, close is not currently invoked on
the unwind path.
3.9 Exceptions
Section titled “3.9 Exceptions”type Error = NotFound String | IoError String
raise (NotFound "config.toml")
try riskyOperation ()catch NotFound path -> "missing: " ++ path IoError msg -> "io: " ++ msg _ -> "unknown failure"endException values are ordinary ADT values. raise throws; try … catch … end
matches the raised value against clauses like a case. An unmatched exception
propagates; an uncaught exception terminates the program with a stack trace.
3.10 Algebraic effects Partial
Section titled “3.10 Algebraic effects Partial”handle perform State.get ()with State.get () resume -> resume 42 return val -> valendperform Effect.op arg requests the operation Effect.op from the nearest
enclosing handle that covers it. A handler clause receives the operation’s
argument and a resume continuation; return val -> … transforms the handled
expression’s normal result.
Function types carry a latent effect expression listing known operations and
any independent open sources: Int -> !{State.get} Int. handle symbolically
masks the operations it covers; application joins a callee’s expression into the
caller without equating independent callbacks. Applying a function whose row is
not fully covered at the top level is error E0202, reported at the
introducing perform with a note at the application site. Pure recursive
components use a least-derived summary; imported and higher-order unknown
sources remain open.
Current limitations. Handlers are shallow, in-scope dispatch: resume is an
identity continuation, not a captured delimited continuation. effect
declarations do not parse yet; operations are identified by their Effect.op
label at perform sites. See Effects for the practical
guide.
3.11 extern (C FFI)
Section titled “3.11 extern (C FFI)”extern sqrt : Float -> Float inextern pow : Float -> Float -> Float insqrt (pow 2.0 10.0) # => 32.0
extern async readFile : String -> String inreadFile "data.txt" # non-blocking; auto-awaited at use
extern inspect : String -> Int = "native_inspect" borrow "1" ininspect ("temporary" ++ "") # native_inspect observes but does not consumeextern name : Type in body declares an external C symbol with a Yona type; the
linker resolves it. Type mapping: Int ↔ i64, Float ↔ double, Bool ↔
i1, String ↔ char*. Curried annotation A -> B -> C denotes a
two-argument C function returning C. The async modifier submits the call to
the runtime’s thread pool and yields a promise, awaited transparently at use
sites (§6.2).
Native declarations have no body from which ownership can be inferred. Append
borrow "01..." after the optional C symbol to mark exactly which parameters
the native function observes without consuming; the mask length must equal the
Yona arity. A caller then retains ownership and releases anonymous temporaries
after the call. This is supported for synchronous and io-completion externs.
Thread-pool async and native-task externs reject masks until their task
contexts can pin borrowed arguments through completion or cancellation.
4. Patterns
Section titled “4. Patterns”| Pattern | Example | Matches |
|---|---|---|
| Literal | 42, "hi", :ok, true |
that exact value |
| Variable | x |
anything; binds x |
| Wildcard | _ |
anything; binds nothing |
| Tuple | (a, _, c) |
tuples of that arity |
| Sequence | [], [x], [a, b] |
sequences of that exact length |
| Head–tail | [h | t], [a, b | rest] |
non-empty sequences; t/rest bind the remainder |
| Constructor | Some x, Rect w h |
values built by that constructor |
| Record | Person{name: n} |
matches named fields; others ignored |
| Dictionary | {"key": v} |
dictionaries containing the key |
| As-binding | [h | t] as whole |
matches the inner pattern and binds the whole value |
| Or-pattern | :a | :b -> … |
either alternative; both must bind the same names |
| Guard | n if n > 0 -> … |
pattern matches and guard is true |
Patterns appear in case clauses, function parameters, let bindings, and
catch clauses. Matching is left-to-right, top-to-bottom, with no backtracking
within a clause.
5. Operators
Section titled “5. Operators”Precedence, highest to lowest; all binary operators are left-associative except
**, ::, and the arrows:
| Level | Operators | Meaning |
|---|---|---|
| 1 | . |
field access |
| 2 | juxtaposition | function application |
| 3 | ** |
power |
| 4 | ! ~ unary - |
logical not, bitwise not, negation |
| 5 | * / % |
multiplicative |
| 6 | + - |
additive |
| 7 | << >> >>> |
shifts |
| 8 | ++ -- |
concatenation; remove elements of the right from the left |
| 9 | :: :> |
cons (prepend); append (snoc) |
| 10 | < > <= >= |
comparison |
| 11 | == != |
equality |
| 12 | & |
bitwise and |
| 13 | ^ |
bitwise xor |
| 14 | | |
bitwise or |
| 15 | in |
membership (seq/set element, dict key) |
| 16 | && |
logical and (short-circuit) |
| 17 | || |
logical or (short-circuit) |
| 18 | |> <| |
pipes |
Sequence-specific operators: x :: xs prepends, xs :> x appends, and
xs ++ ys concatenates. a -- b removes every element of b from a
(sequence difference; set difference on sets). x in coll is a membership test
on a sequence or set, or a key test on a dictionary. The same keyword closes
let and with, so membership in a let-binding RHS that is itself a let,
if then/else, lambda, perform argument, raise, or with body must be
parenthesized: then (2 in xs), let z = 1 in (2 in xs). if 2 in xs then …
is fine — the condition is not a terminator context.
6. Evaluation model
Section titled “6. Evaluation model”6.1 Strictness
Section titled “6.1 Strictness”Yona is strictly evaluated: arguments are evaluated before application, and
bindings before their bodies — with the single systematic exception of
asynchronous values (§6.2). There is no lazy evaluation; laziness is expressed
explicitly with thunks (\-> e) or Iterator/Std\Stream pipelines.
6.2 Transparent asynchrony
Section titled “6.2 Transparent asynchrony”Functions that perform I/O (and extern async functions) return a promise
internally. The type system tracks promises invisibly: when a promise appears
where its underlying value is required — as an operator operand, function
argument, or condition — the compiler inserts an await coercion. Users never
write async or await, and no function is “colored”.
Because let bindings without mutual dependencies have no defined order,
independent asynchronous bindings are submitted before any is awaited:
let a = readFile "foo.txt", # submitted b = readFile "bar.txt" # submittedin a ++ b # both awaited here; elapsed ≈ max, not sumImplementation note. On Linux, file and network I/O submit to io_uring; CPU-bound async work runs on a work-stealing thread pool. Buffers passed to in-flight kernel operations are pinned. See Concurrency in depth.
6.3 Parallel generators
Section titled “6.3 Parallel generators”[| f x for x = xs ] evaluates f over the elements concurrently on the thread
pool and preserves order in the result.
6.4 Memory
Section titled “6.4 Memory”Values are managed by atomic reference counting with recursive destructors; there is no tracing garbage collector and no stop-the-world pause. The compiler applies Perceus-style ownership transfer (callee-owns calling convention), uniqueness-based in-place mutation for uniquely owned values, and escape analysis that arena-allocates values proven not to escape. See Memory and linearity.
7. Types
Section titled “7. Types”7.1 Inference
Section titled “7.1 Inference”The type system is Hindley–Milner: every expression has a principal type,
inferred without annotations. Optional annotations (name : Type preceding a
definition) are checked, not trusted. Polymorphic functions are compiled by
monomorphization — one native instantiation per concrete type used.
7.2 Algebraic data types
Section titled “7.2 Algebraic data types”type Option a = Some a | Nonetype Result a e = Ok a | Err etype Tree a = Leaf | Node (Tree a) a (Tree a)type Lazy a = Cons a (() -> Lazy a) | Empty # function-typed fieldtype Person = Person { name : String, age : Int } # named fieldsConstructors are first-class functions. Named-field types support dot access, record patterns, and functional update:
let p = Person { name = "Ada", age = 36 } in(p.name, p { age = 37 })Implementation note. Non-recursive ADTs compile to flat {tag, payload}
structs; recursive ADTs and ADTs with function-typed fields are heap-allocated.
7.3 Traits Stable
Section titled “7.3 Traits Stable”trait Eq a eq : a -> a -> Bool neq : a -> a -> Bool neq x y = if eq x y then false else true # default methodend
instance Show a => Show (Option a) show opt = case opt of Some x -> "Some(" ++ show x ++ ")" None -> "None" endend
trait Eq a => Ord a # superclass constraint compare : a -> a -> OrderingendTraits are type classes resolved statically: each call site compiles the
concrete instance directly (monomorphization), with no runtime dispatch cost.
Instances are always public; export trait Name exports a declaration. Equality
operators select Eq; relational operators select Ord and interpret
Less | Equal | Greater. The Prelude’s collection, algebra, conversion,
parsing, and erased concurrency marker traits use the same resolution model. See
Traits.
7.4 Effect rows Partial
Section titled “7.4 Effect rows Partial”Function arrows carry a normalized effect expression such as
a -> !{State.get} Int. Effects are inferred, losslessly unioned at
application, symbolically masked by handle, and propagated through .yonai
interfaces. The canonical effectscheme field preserves all arrow positions,
independent open sources, and masks. §3.10 lists current limitations.
7.5 Linear types Partial
Section titled “7.5 Linear types Partial”Linear a marks values that must be consumed exactly once: file handles,
sockets, process handles, channel endpoints. The linearity checker rejects
duplication and silent dropping; with is the idiomatic consumer. @borrow
marks parameters that use a linear value without consuming it. See
Memory and linearity.
7.6 Row-polymorphic records Stable
Section titled “7.6 Row-polymorphic records Stable”Record types unify by row: a function using r.name accepts any record
containing a name field of the right type, and the residual row is
polymorphic.
8. Modules
Section titled “8. Modules”module Data\Geometry
export area, perimeterexport type Shape
type Shape = Circle Float | Rect Float Float
area s = case s of Circle r -> 3.141592653589793 * r * r Rect w h -> w * hend
perimeter s = case s of Circle r -> 2.0 * 3.141592653589793 * r Rect w h -> 2.0 * (w + h)endA module is a top-level declaration — not an expression — and extends to end
of file. export statements name exported functions; export type T exports a
type with all its constructors. export type T opaque exports only the nominal
type, hiding its constructors from importers; export f from Other\Module
re-exports. Module names are backslash-separated paths (Std\List).
Imports are expressions:
import map, filter from Std\List in … # selectiveimport length as len from Std\String in … # aliasedimport Std\Math in … # whole moduleStd\List::map (\x -> x + 1) [1, 2, 3] # fully qualified, no importImplementation note. A module compiles to a native object file with C-ABI
exports (mangled YonaPkgModFunc) and a .yonai interface file carrying
types, effect summaries/schemes, linearity, and — for generic functions — the
source text itself (GENFN), so a caller with new concrete types can
re-monomorphize the function locally. yonac -I path adds interface search
paths. See Modules and interfaces.
9. Conformance and diagnostics
Section titled “9. Conformance and diagnostics”Compiler diagnostics carry stable codes (E0100-style, W… for warnings).
yonac --explain E0202 prints the full explanation for a code. The
error code index lists user-facing codes and their
meanings.