Syntax and evaluation
Yona is an expression language: there are no statements. A program is a single expression, and evaluating it produces the program’s result. This page covers the lexical ground rules — how expressions begin and end, what literals look like, and how they evaluate.
Everything is an expression
Section titled “Everything is an expression”Every construct — if, case, let, do, function bodies — is an
expression with a value. There is no return keyword and no reason to pad
a body with a dummy 0: a function’s value is the value of its body.
let status = if ready then :ok else :waiting inlet label = case status of :ok -> "ready" :waiting -> "hold on"end inlabel # => "ready" (when ready is true)Strict evaluation
Section titled “Strict evaluation”Yona evaluates strictly: arguments are evaluated before a function is
applied, and let bindings are evaluated when bound, not when first used.
Order among independent let bindings is not guaranteed (independent
asynchronous bindings may even run in parallel); when side-effect order
matters, use a do block, whose expressions always run top to bottom.
import print from Std\IO indo print "first" # guaranteed to run before the next line print "second"endNewlines and semicolons
Section titled “Newlines and semicolons”Newlines are significant tokens. A newline (or an equivalent ;) terminates
an expression in the three places where consecutive expressions can appear:
- arms of a
caseexpression, - steps of a
doblock, - function definitions in a module body.
case x of :ok -> handleOk x # newline ends this arm :error -> handleError x _ -> fallback xend
# Semicolons are interchangeable with newlines:case x of :ok -> 1; :error -> 2; _ -> 0 endNewlines are suppressed (treated as plain whitespace) in two situations, which is what makes multi-line expressions natural:
- Inside brackets —
(),[],{}:
let list = [ 1, 2, 3, 4, 5, 6] in list # => [1, 2, 3, 4, 5, 6]- After a binary operator or a continuation token (
->,=,,), so a line ending in an operator continues on the next line:
let total = price + tax + shipping in totalImplementation note. The lexer tracks bracket depth and the previous token
to decide whether a newline is a delimiter or whitespace; inside a
case/do block nested in brackets, newlines still reach the parser as
clause separators. This is what allows juxtaposition application (f x y)
without ambiguity at expression boundaries.
Comments
Section titled “Comments”Line comments start with #. Doc comments start with ## and are attached
to the following definition (the stdlib’s API reference is generated from
them). Block comments use /* … */ and nest.
# a line comment
## Doubles a number. (doc comment — extracted into API docs)double x = x * 2
/* block comment /* nested block comments are fine */ still inside the outer comment */double 21 # => 42Never write -- for a comment — -- is the remove operator token, not a
comment introducer, and will produce a parse error.
Literals
Section titled “Literals”Integers
Section titled “Integers”Int is a 64-bit signed integer. Underscores may separate digits for
readability.
42-171_000_000 # => 1000000Floats
Section titled “Floats”Float is a 64-bit IEEE double. Scientific notation is supported.
3.14-0.51.23e-4 # => 0.000123Strings
Section titled “Strings”Strings are written in double quotes and support the usual escapes
(\", \\, \n, \t, …).
"Hello, World!""Escaped \"quotes\" and \n newlines"Strings interpolate expressions in braces: {name} for a plain variable,
{(expr)} — with parentheses — for anything containing operators or
application. Non-string values are converted automatically.
let name = "World" in "Hello {name}!" # => "Hello World!"let x = 6 in "result is {(x * 7)}" # => "result is 42"Characters and booleans
Section titled “Characters and booleans”'a''\n'truefalse() is the unit value — the empty tuple, used where there is nothing
meaningful to return.
() # => ()Symbols
Section titled “Symbols”Symbols are interned constants written as :snake_case. Two occurrences of
the same symbol are always the same value.
:ok:error:not_foundImplementation note. Symbols are interned to 64-bit integer IDs at compile time, so comparing two symbols is a single integer comparison, and pattern matching on symbols compiles to an integer switch. See Pattern matching.
Conditionals
Section titled “Conditionals”if is an expression and the else branch is mandatory — every if must
produce a value of either branch. Both branches must have the same type.
if x > 0 then "positive"else if x < 0 then "negative"else "zero"Prefer case over long if/else chains when you are matching on the
shape of a value — see Pattern matching.
Operator precedence
Section titled “Operator precedence”From highest to lowest binding strength:
- Field access (
.) - Function application (juxtaposition —
f x) - Power (
**) - Unary (
!,~, unary-) - Multiplicative (
*,/,%) - Additive (
+,-) - Shift (
<<,>>,>>>) - Join (
++) - Cons (
::,:>) - Comparison (
<,>,<=,>=) - Equality (
==,!=) - Bitwise AND (
&) - Bitwise XOR (
^) - Bitwise OR (
|) - Membership (
in) - Logical AND (
&&) - Logical OR (
||) - Pipe (
|>,<|)
Function application binds tighter than every binary operator, so
f x + g y parses as (f x) + (g y):
let f x = x * 10, g y = y + 1 inf 2 + g 3 # => 24, i.e. (f 2) + (g 3)The full grammar and operator semantics are in the language specification.
Where to next
Section titled “Where to next”- Functions — definitions, lambdas, application, pipes.
- Pattern matching —
caseand every pattern form. - Types and data — inference, ADTs, records, traits.
- Collections — sequences, dictionaries, sets, generators.