Statements#
Every statement ends with a period (.).
Print "Hello, World!".
Case Sensitivity#
Keywords are case-insensitive. These are equivalent:
Print,print,PRINTIf,if,IF
Comments#
Comments use parentheses ( ) — just like parenthetical remarks in natural language writing.
(This is a comment)
print "Hello".
print "World". (end of line comment)
a number (the counter) called x is 5.
(Multi-line comments
work naturally across
several lines)
(Nested (parentheses (are supported)) too)
Comments can appear:
- On their own line
- At the end of a statement
- In the middle of a statement (between tokens)
- Spanning multiple lines
Paragraph Breaks (Blank Lines)#
Blank lines (paragraph breaks) organize code into logical sections. They are optional and have no effect on program execution between two fully-terminated top-level constructs — for example, between two function definitions or between two complete statements at the top level.
Inside an open clause they are not cosmetic: a blank line force-closes every clause that is still open, including an enclosing function definition. Use a blank line to end a construct deliberately (after a while, for each, repeat, on error, or nested if body), not to add visual spacing in the middle of a body.
print "Section 1".
print "Section 2".
Note: A function definition is closed by a blank line (paragraph break) — this is required, not a style convention. A period closes only the innermost open clause (rule 1 below), so the period ending a body statement does not close the function. Without a blank line after the body, every statement following the signature is absorbed into the function body; since the function is typically not called from within itself, the program then silently does nothing (exit 0, no output). A following To or Library does begin a new top-level construct and so ends the body, but any other statement is absorbed. The compiler warns when a function definition is still open at end of file. See The termination rule below.
Sentence Consumption#
Action-consuming constructs (loops, conditionals, error handlers) consume the entire sentence they appear in. Multiple actions within that sentence are separated by commas.
(Single action)
While x is less than 10, increment x.
(Multiple comma-separated actions in one sentence)
While x is less than 10, print x, increment x.
(For loops work the same way)
For each number from 1 to 10, print the number, print " ".
(Error handlers too)
On error print "Something went wrong", exit 1.
(If/else with multiple actions)
If x is greater than 10 then, print "big", set y to 1. Otherwise, print "small", set y to 0.
Key Rules:
- Period (
.) ends the entire construct, including all its actions - Comma (
,) separates multiple actions within the same construct - Only function definitions can span multiple sentences (using paragraph breaks)
Sentence ownership (nested constructs):
- A nested construct (especially
if ... then) owns its own trailing period. - Outer constructs (
while,for each,repeat) do not steal that inner period. - After an inner
ifends, the outer sentence may continue with more actions.
While content is not empty,
if number_lines then,
print "{line}: " without newline.
write content to output,
read line from source into content.
In the example above, the period after the inner if closes only that if. The while body continues with write and read.
The termination rule#
Two rules govern where a construct's body ends, and together they explain everything above precisely:
- A period closes the most recently opened clause — the innermost one currently open (
if,on error,for,while,repeat), and only that one. This is why the nestedifexample above works: its period closes theif, not thewhile. One period closes one level; to close more than one, write more than one — see Closing more than one level. - A blank line (paragraph break) force-closes every open clause at once — including an enclosing function definition. Think of nested HTML
<div>s: a paragraph boundary closes all of them together, the same way you would never continue a single sentence across a paragraph break in English.
(A blank line closes everything still open, not just the nearest thing)
a number called retries is 0.
While retries is less than 3,
if retries is equal to 1 then, print "retrying".
the retries is retries add 1.
Print "done".
This prints retrying once (when retries is 1) then done once, after the loop runs its full three iterations — the blank line closes the while (rule 2) even though the if's own period already closed the if (rule 1); there is nothing special about the if being the loop's last action, the blank line would close the loop the same way after any kind of action.
This applies uniformly — while, for each, repeat, and on error all terminate their body on a blank line, regardless of what the last body statement was (an ordinary statement, an if/on error, or another nested loop).
Caution: because rule 1 means a nested construct's own period doesn't close its parent, a blank line placed purely for visual readability inside a loop body — after a nested if or a nested loop, before more of the same loop's actions — will close that loop early, not just add whitespace:
(This blank line is NOT cosmetic - it ends the outer while)
a number called round is 0.
While round is less than 2,
the round is round add 1,
For each item in batch,
print item.
print "batch done".
This prints 1 2 1 2 batch done — not 1 2 batch done 1 2 batch done as the indentation suggests. print "batch done". runs once, after the loop, not once per batch, because the blank line closed the while right after the nested for each closed itself.
This can hang your program with no error message, if the ejected statement happens to be the loop's own increment:
(DON'T DO THIS - infinite loop, no diagnostic, the blank line ejects the increment)
a number called counter is 1.
While counter is less than or equal to 2,
For each k from 1 to 2,
print "inner {k}".
increment counter.
Print "end".
increment counter. is ejected from the while body by the same blank line, so counter never changes and the loop never becomes false — it hangs forever, printing inner 1 / inner 2 on repeat, and Print "end". never runs. There is no error, no warning, and nothing in the output points at the blank line as the cause. If a loop that should terminate hangs instead, check for a blank line inside its body first.
A blank line placed after a comma (mid-sentence, more actions still to come) is the one exception — it is still just visual spacing there, since the sentence is explicitly still open:
(Safe: this blank line follows a comma, so it stays cosmetic)
While retries is less than 3,
print "attempt {retries}",
increment retries.
Closing more than one level#
Rule 1 closes exactly one level, and rule 2 closes all of them. When you are nested several levels deep and want to come back up some of the way, periods stack: write one period per level you want to close.
(Three nested ifs, so three periods to get all the way back out)
a number called n is 0.
If n is equal to 1 then,
If n is equal to 1 then,
If n is equal to 1 then, print "innermost"...
print "back at the top".
This prints back at the top. The three periods close the innermost if, then the middle one, then the outer one, so print "back at the top". runs at the top level. Written with one period or two, it would still be inside an if whose condition is false, and would print nothing at all — with no error.
Indentation is not what decides this. Vox ignores leading whitespace entirely, so a program can be minified without changing its meaning; the period count is the only thing that closes a clause.
This is how you choose which if an Otherwise belongs to#
An Otherwise (or But if) continues the innermost if that is still open. Closing that if first is therefore how you hand the Otherwise to an enclosing one. These two programs differ by a single character and behave differently:
(ONE period: the Otherwise belongs to the INNER if)
a number called m is 5.
If m is equal to 1 then,
If m is equal to 2 then,
print "inner then".
Otherwise,
print "outer else".
Print "done".
prints only done. The Otherwise continued the inner if, so the whole construct sits inside If m is equal to 1, which is false — nothing in it runs.
(TWO periods: the inner if is closed, so the Otherwise belongs to the OUTER one)
a number called m is 5.
If m is equal to 1 then,
If m is equal to 2 then,
print "inner then"..
Otherwise,
print "outer else".
Print "done".
prints outer else then done, which is what the indentation in both versions suggests — but only the second one actually says it.
An empty Otherwise,. closes an inner chain the same way and is easier to read than a run of periods, since it names the thing being closed instead of asking you to count:
(Same result as two periods, spelled out instead of counted)
a number called m is 5.
If m is equal to 1 then,
If m is equal to 2 then,
print "inner then".
Otherwise,.
Otherwise,
print "outer else".
Print "done".
This also prints outer else then done. The first Otherwise,. takes the inner if's else branch and does nothing with it, which closes that chain; the second one is then free to continue the outer if.
Get the count wrong and nothing tells you. Too few periods and the following statements are absorbed into a clause you thought you had left; too many and they escape one you meant to stay in. Either way the program still compiles and still runs. If a branch never seems to execute, or a loop that should finish hangs instead, count the periods between it and the construct it belongs to — and remember the hanging case is the same one described above under rule 2: the absorbed statement is the loop's own increment.
Ranges#
Ranges define a sequence of numbers from a start to an end value. They are not allocated as lists - they compile directly to efficient loop constructs with a counter, bounds check, and increment.
(Basic range in for-each loop)
For each number from 1 to 10, print the number.
(Range with variable bounds)
Set start to 1.
Set end to 5.
For each number from start to end, print the number.
(Range in loop expansion - see below)
print each number from 1 to 10.
Key points:
- Ranges are inclusive -
1 to 5includes 1, 2, 3, 4, and 5 - Ranges compile to efficient assembly loops, not list allocations
- The loop variable (
the number) is available inside the loop body
Loop Expansion#
The each...from syntax is a universal loop expansion that works with any action. It transforms a single action into a loop that executes for each item in a collection or range.
(Print each item from a list)
print each number from [1, 2, 3].
(Print each number from a range)
print each number from 1 to 15.
(Call a user function for each item)
process of each item from mylist, print "done".
(Open a file for each argument)
a buffer called content.
open a file for reading called source at each filename from arguments's all,
read from source into content,
print the content,
close source.
Syntax: <action> each <variable> from <collection>, <additional actions>
The action executes once per item in the collection or range, with the loop variable bound to each item. Additional comma-separated actions execute inside the loop after the main action.
Works with:
print each X from Y- print each itemfunction of each X from Y- call function for each itemopen ... at each X from Y- open file for each path- Any action that takes an argument
Supported collections:
- Ranges:
1 to 10,start to end- numeric sequences - Lists:
[1, 2, 3], any list variable arguments's all- all command-line arguments (argv[1..])
Chained each clauses — a grid#
More than one each <variable> from <collection> clause may appear in a single sentence, joined by and. The action then runs once per element of the Cartesian product of the collections, in row-major order — the leftmost clause is the outermost loop, exactly as if the clauses were nested For each loops written left to right:
'pair' of each x from [1, 2] and each y from [10, 20].
runs 'pair' four times — (1,10), (1,20), (2,10), (2,20) — identical to:
For each x from [1, 2],
For each y from [10, 20],
'pair' of x and y.
There is no limit on the number of clauses. A fixed (non-each) argument may sit among them in any position, and is evaluated once per call:
'pair' of 5 and each y from [10, 20]. (fixed first, then expansion)
'pair' of each x from [1, 2] and 99. (expansion first, then fixed)
An inner collection may use a variable bound by an outer clause, giving triangle iteration:
'pair' of each row from [1, 2, 3] and each col from 1 to row.
A range bound in an each clause takes a primary, not an expression — each col from row add 1 to 4 is a parse error. Brace an arithmetic bound: each col from {row add 1} to 4.
See Loop Expansion with Collections below for the arity rule, the empty-collection rule, duplicate loop variables, and after-loop values.
Conditional Branching with but if#
The but if clause is a generic conditional branch over a base action. It is available in both for each loops and loop expansion (<action> each ... from ...).
(FizzBuzz example - print number, but override with word if divisible)
print each number from 1 to 15,
but if the number modulo 6 is equal to 0 print "fizzbuzz",
but if the number modulo 2 is equal to 0 print "fizz",
but if the number modulo 3 is equal to 0 print "buzz".
(Simple even/odd labeling)
print each number from 1 to 10,
but if the number modulo 2 is equal to 0 print "even".
(Append to a list with a conditional override)
append each number from 1 to 5 to out,
but if the number modulo 2 is equal to 0 append 0.
(With for-each loop)
For each number from 1 to 15,
print the number,
but if divisible of the number and 3 is true print "divisible by 3".
Syntax: <base action>, but if <condition> <alternative action>, but if <condition> <alternative action>, ... [otherwise <default alternative action>].
How it works:
- The default action is the base statement.
- Each
but ifclause is checked in order. - If a condition is true, that alternative action runs instead of the default.
- If no conditions match, the default action runs.
- An optional trailing
otherwiseclause provides a final alternative.
Key points:
- Conditions are checked in order - first match wins
- Multiple
but ifclauses can be chained - The alternative action can be any valid Vox statement
otherwiseprovides a catch-all alternative- Works with both ranges and collections
- The loop variable (
the number) is available in conditions - In an
appendbranch, theto <list/buffer>target may be omitted and is inherited from the base append statement; retargeting to a different list/buffer is not allowed
Inline Substitution with treating#
The treating X as Y clause performs inline value substitution - like bash's ${var//X/Y} but readable.
(Replace '-' with "/dev/stdin" for each filename)
open a file for reading called source at each filename from arguments's all treating "-" as "/dev/stdin",
read from source into content,
write content to output,
close source.
(Print with default value)
print each name from names treating "" as "Anonymous".
(Call function with substitution)
process of each filename from files treating "-" as "/dev/stdin".
(Append with substitution - the clause goes with the `each` clause, before
the `to <destination>`)
append each name from names treating "" as "Anonymous" to cleaned.
Syntax: ... each <var> from <collection> treating <match> as <replacement>, ...
If the loop variable equals <match>, it's replaced with <replacement> for that iteration.
Equality is by type as well as by value: a <match> whose type differs from the element's never fires, and that element comes through unchanged — and where the compiler can prove the mismatch, it says so at compile time instead. Where the element, the <match> or the <replacement> is a value, the runtime tag it carries is what the comparison reads, and a substitution that fires hands the <replacement>'s own type out with it.