Reference

The whole reference, on one page.

Every section of the specification in order, as one document.

This page is the entire specification, around 5,300 lines of it. It is here for skimming and for printing. If you already know roughly what you are after, a section page is quicker to load and easier to read.

Generated from LANGUAGE.md at bf2cba0, 2026-08-22.

Basics

Statements

Every statement ends with a period (.).

Print "Hello, World!".

Case Sensitivity

Keywords are case-insensitive. These are equivalent:

  • Print, print, PRINT
  • If, 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 if ends, 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:

  1. 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 nested if example above works: its period closes the if, not the while. One period closes one level; to close more than one, write more than one — see Closing more than one level.
  2. 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 uniformlywhile, 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 5 includes 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 item
  • function of each X from Y - call function for each item
  • open ... 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:

  1. The default action is the base statement.
  2. Each but if clause is checked in order.
  3. If a condition is true, that alternative action runs instead of the default.
  4. If no conditions match, the default action runs.
  5. An optional trailing otherwise clause provides a final alternative.

Key points:

  • Conditions are checked in order - first match wins
  • Multiple but if clauses can be chained
  • The alternative action can be any valid Vox statement
  • otherwise provides a catch-all alternative
  • Works with both ranges and collections
  • The loop variable (the number) is available in conditions
  • In an append branch, the to <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.

Types

TypeKeywordDescription
IntegernumberWhole numbers
FloatfloatFloating-point numbers (64-bit IEEE 754)
StringtextText strings
Booleanbooleantrue or false
ListlistCollection of items
MapmapKey/value collection (JSON object; text keys)
BufferbufferMemory block for I/O (dynamic or fixed-size)
FilefileFile descriptor handle (auto-cleaned)
TimetimeDate/time value (unix timestamp with components)
TimertimerStopwatch for measuring durations
Thingthing (contextual)User-defined composite value type — see Things

Variables

Declaration with Type

Use a or an before the type to declare a new variable:

a number called x is 5.
a text called name is "Alice".
a boolean called done is true.
a list called nums is [1, 2, 3].
a map called person is {"name": "Alice", "age": 30}.

Declaration with Set/Create

Set a number called counter to 1.
Create a text called greeting to "Hello".

Two Canonical Forms

Every declarable type supports two equivalent forms, both routed through the same type resolver:

  • A TYPE called NAME is VALUE. — declares NAME and initializes it to VALUE immediately. Set/Create with to <value> (above) is the same form with a different lead-in word.

  • Create a TYPE called NAME. — declares NAME with no initializer and gets that type's default (zero) value:

    Create a number called n.       (n is 0)
    Create a float called f.        (f is 0.0)
    Create a boolean called b.      (b is false / 0)
    Create a list called items.     (items is [])
    Create a map called m.          (m is {})
    Create a buffer called buf.     (buf is empty, 0 bytes, dynamic capacity)
    Create a value called v.        (v is nothing)
    Create a timer called t.        (t is ready to Start)
    TypeDefault on bare Create
    number0
    float0.0
    textempty string
    booleanfalse (0)
    list[]
    map{}
    bufferempty (0 bytes)
    valuenothing
    timerready to Start
    filenot supported — see below
    timenot supported — see below

    file and time require an initializer. A default file or time value would be meaningless (no path to open, no timestamp to hold), so Create a file called N. and Create a time called N. are both rejected at compile time with a message naming what to supply:

    Create a file called src.
    (compile error: A file variable must be initialized with a path
       Example: a file called source is "input.txt".)
    
    Create a time called clk.
    (compile error: A time variable must be initialized
       Example: a time called now is current time.)

    Give them a value with the first canonical form instead: a file called source is "input.txt". / a time called now is current time.

Declaration Order

Top-level statements run in the order they are written, so a variable must be declared above the code that reads it. Reading a top-level variable before its own declaration is a compile-time error:

Print label.                     (compile error: 'label' is used before it is declared)
a text called label is "hello".

A function body is the exception, and for the same reason: a function runs when it is called, not where it is written, so a body may name a global declared further down the file — see Function Scope.

Assignment (Existing Variable)

Use the to reference an existing variable:

the x is 10.
the counter is the counter add 1.

Type Immutability

A variable's type is fixed at its declaration and never changesvalue is the one deliberate exception, covered below. Every form that writes to an already-declared name — x is <value>., the x is <value>., and Set x to <value>. — is checked the same way: if the new value's type doesn't match the type x was declared with, that's a compile error, not a silent retype.

a number called n is 5.
n is "abc".              (compile error: cannot assign text to 'n', which is a number)
n is "42" as a number.   (OK: n is now 42)

The error names the variable, its declared type and where it was declared, the type of the value that doesn't match, and the exact cast that would fix it:

error: cannot assign text to 'n', which is a number
  --> prog.vox:2:1
   |
 2 | n is "abc".
   | ^ this assigns text
   |
  note: 'n' was declared as a number at prog.vox:1:17
  help: convert it explicitly:  n is "abc" as a number.

Convert explicitly with Type Casting (as a number / as text / ...) — the same mechanism used everywhere else in the language, not new syntax for this rule.

This isn't limited to reassignment. Any construct that binds a name to a new runtime value is checked the same way: reusing an already-declared name as a For each/for-range loop variable, as the target of open ... called, or as the target of Allocate ... for all reject a type that conflicts with the name's existing declaration. So does a nested declaration that reuses an outer name with a different type (Vox has no block-level scoping today, so there is no separate slot for the inner declaration to occupy):

a number called n is 5.
If 1 is equal to 2,
  a text called n is "abc".   (compile error: cannot bind 'n' to text in this
                                 declaration; 'n' is already declared as a number)

Two exemptions, both deliberate:

  • Buffers. Writing into a buffer (b is 42., Set b to "text".) copies the value's text representation into the buffer's content — a format operation, not a type change — so a buffer accepts any value type on every write.
  • value. A variable declared a value called x is the language's sanctioned dynamic type and keeps accepting any type across reassignment, exactly as documented in Dynamic Values (value) below — that section's behavior is unchanged by this rule, not an exception carved out of it. This also covers the in-place retype statement <valuevar> is a <type>. (e.g. numstr is a number.), which reads the variable's runtime tag, converts the value, and updates the tag in place — see "A value can be retyped in place" below. The same statement applied to a statically-typed variable (n is a text. where n is a number) is still rejected by this rule exactly like any other mismatched assignment; only a value-declared name can be retyped.

What this doesn't catch. The check only rejects a mismatch it can prove statically from the value's own shape (a literal, a cast, a read from a list/map whose element type is provably uniform, a 's <property> read whose property has the same type whatever it is read from — every property in the tables under Object Properties except first, last, absolute, duration and elapsed, whose type follows the thing they are read from — ...). A value coming from a function call, an unprovable list/map read (a map literal with mixed value types, for instance), or anything else the compiler can't classify at compile time is allowed through unchecked. This closes a large, concrete class of bugs — a variable's compiler-tracked type disagreeing with what it actually holds at runtime — not every possible source of type confusion, and it says nothing about type agreement across a .lib import boundary (a library's declared signature is currently trusted, not verified against its .so).

Naming Rules

A name is an identifier, never a string literal. Three forms, no overlap, no context-sensitivity:

FormMeaningExample
"..."String literal. Always. Everywhere.print "hello".
bare_wordIdentifier, single worda number called total is 5.
'multi word'Identifier, contains spacesa number called 'total items' is 5.
  1. "..." is never an identifier, in any position. Where an identifier is expected and a string literal is found, that is a compile error.
  2. A bare identifier matches [A-Za-z_][A-Za-z0-9_]* and is not a reserved keyword. Reserved keywords remain rejected as names — so a flag named number or version must be written 'number' / 'version'.
  3. A quoted identifier is '' containing two or more characters and no newline. Exactly one character between single quotes remains a character literal ('A') — that is why single-character quoted identifiers do not exist. Write x, not 'x'.
  4. Single-word quoted identifiers ('total') are legal but non-canonical; they lex identically to the bare form. Prefer bare.
  5. Possessive. 'name's length is canonical: after a closing identifier quote, an s immediately following (no space) and itself followed by a non-identifier character lexes as the possessive marker. 'name''s also works; both are accepted.
  6. These are data, not names, and stay double-quoted: map keys (person's "name"), file paths (see "./utils.vox"), flag aliases ("-v"), and versions (version "1.0").

See Names and strings for why one token cannot mean two things.

Names and strings

One token cannot mean two things. "..." is a string literal everywhere, and a name is a bare or single-quoted identifier:

a number called "x" is "get five".

That is a compile error — is "get five" rejects the string in identifier position and points you at 'get five'. Were it accepted, "get five" in expression position would read as a string literal — a pointer to the function's code — and x would quietly receive that pointer as a number: a wrong answer that looks like data, with no error and no warning.

Functions

Definition

To <function name> with a <type> called <param1> and a <type> called <param2>. Return a <type>, <expression>.

No-parameter functions are also valid:

To 'show version'.
  Print "1.0.0".

To ping.
  Print "pong".

Examples:

To 'add numbers' with a number called x and a number called y. Return a number, the x add y.

To 'check divisibility' of a number called divisor and a number called dividend. Return a boolean, the divisor modulo the dividend is 0.

Rules:

  • Function name is a bare single word (add) or a single-quoted multi-word name ('add numbers')
  • Parameters are optional. If present, introduce them with with or of (both work identically)
  • Parameters use a <type> called <name> syntax (bare if single-word, 'single-quoted' if it contains spaces)
  • Multiple parameters joined with and
  • Return type follows Return a <type>,

Function Scope

  • Variables declared at top level are global and can be used inside functions — including inside a function written above the declaration, because the body runs when it is called, and it reads the global as its declared type either way. A function that runs before the declaration has been reached reads the type's empty value — "", [], {}, an empty buffer, 0, 0.0, false — never the value the declaration will go on to store. Top-level code has no such licence: see Declaration Order.
  • Variables declared inside a function are local to that function and are not available at top level.
  • Referencing an unknown variable inside a function is a compile-time error.
  • Assigning to a top-level variable inside a function (Set g to ... / the g is ...) mutates the global itself, so the new value is visible after the call returns and to every other function.
  • Declaring a variable inside a function shadows a top-level variable of the same name (a number called g is 5. inside a function creates a local g); the global is left untouched. Recursion still gets a fresh set of locals per call. This applies to value too: its payload and runtime type tag are stored as a pair, in whichever storage (function-local, or the top-level global's own pair of storage locations) that particular value uses, so a mutation inside one function is never visible to another unless it is genuinely the same global.

Parameter and Local Types

Parameters may use any of the 11 expressible types — number, float, text, boolean, list, map, buffer, file, time, timer, value — and a typed parameter supports the same properties and operations as a top-level variable of that type. The same 11 types are also legal as a declared Return a <type>, return type — parameters and returns share one vocabulary, not two. A parameter (or return type) may also be value, the dynamic type whose runtime tag travels with its payload across the call (a map rides this as payload + tag 5); see Dynamic Values (value) below.

To 'contains token' of a buffer called hay and a text called devname.
  a buffer called needle is " {devname}\n".
  a number called H is hay's size.
  a number called b is byte 1 of hay.
  ...

Key points:

  • Buffer parameters support 's size/'s empty/'s full and byte access; list parameters support 's length/element access; map parameters support 's length/'s empty/'s keys/'s values and keyed access, and print as a whole map (print holder., "{holder}"); file parameters support file properties.
  • A buffer parameter is the caller's buffer, and stays the caller's buffer across a growth: an append, a resize, or a byte written past the current capacity moves the block, and the caller's variable follows it there. Declaring a buffer of the same name inside the function names a different buffer from that point on and leaves the caller's alone; Set <parameter> to ... is not a rebinding — on a buffer it copies bytes into the buffer the parameter already names, so the caller sees the new bytes.
  • Buffers declared inside a function body work with every initializer form, including format strings (is " {devname}\n").
  • A function call's declared return type is tracked through assignment: reassigning an existing variable from a call (the label is classify of n.) preserves the correct type.

A collection parameter is the caller's collection

A list or map parameter names the caller's collection, not a copy of it. Whatever the function does to it - setting an element, appending, inserting a key, growing it past whatever size it started at - is what the caller's variable holds when the call returns, however many calls deep the collection was passed:

To 'add one to' with a list called items.
    append "x" to items.

a list called xs is ["a"].
'add one to' of xs.
'add one to' of xs.
Print xs's length.    (prints: 3)

This is the opposite of a thing parameter, which is a copy (see Things below): a thing has no reference to share, and a collection is nothing but one. Only a variable can be written back to - a collection built at the call itself ('the size of' of [1, 2, 3]) has nowhere to return growth to, so the function may read and grow it freely but the growth goes nowhere once the call ends.

A .so with a list or map parameter and the programs that see it must be built by the same version of Vox.

Function Calls

In expressions, use the function name followed by of, to, with, or on and arguments:

'add numbers' of 3 and 5
'check divisibility' of the number and 6
calculate with x and y

Rules:

  • Function name is a bare single word (calculate) or a single-quoted multi-word name ('add numbers')
  • For calls with arguments, use of, to, with, or on
  • Multiple arguments separated by and

Calls with no arguments can be written directly:

'show version'.
ping.

Calling as Statement

Print 'add numbers' of x and y.

Reading a result

Return a <type>, is optional in the grammar, but it decides where the result may be read. A declared return type travels to every call site, so the result can be printed, interpolated, stored in a list or map slot, or put in a value — each of those reads it back as what it is.

A result with no declared return type has nothing to be read as, and the compiler will not guess: it is accepted only where the position itself supplies the type, and refused where nothing does.

To 'opaque label'. Return "hi".

a text called saved is 'opaque label'.   (fine — the declaration says text)
print saved.                             (prints: hi)

print 'opaque label'.                    (compile error: no declared return type)

Positions that supply a type: a declared variable's declaration, a later assignment to one, and an argument landing on a declared parameter. Every other position — print <call>, a {...} interpolation, append, a list literal slot, set element, a map value, a value declaration — needs the return type declared. See Mixed-Type Lists.

Things

Vox's eleven builtin types are the compiler's own composite values — a buffer is [capacity][length][flags][data] with 's reading a field at a fixed offset. A thing opens that same mechanism to the program: a user-defined composite value type, built from named fields, with every offset fixed at compile time. No vtables, no dispatch, no runtime component — a thing is a layout, copied, printed, and compared by the compiler the way a buffer is.

A thing is defined once, at the top level, and its name then works everywhere a builtin type keyword works: in declarations, parameters, and return types. A definition declares a type — it allocates nothing and emits no code, so the only output around it comes from the ordinary statements.

examples/delivery.vox is a complete program built from two things of its own: it declares them, makes one with a manifest member, nests one inside the other, copies, prints, and compares them.

Defining a thing

A thing called point has
  a number called x is 0,
  a number called y is 0.

Print "defined".

The keyword is thing, and the verb is has. A definition has two kinds of entry:

  • a data fielda <type> called <name>, with an optional is <literal> default;
  • a function membera function called <name>, the manifest (see The manifest below).

A field without a default takes its type's zero value. thing is a keyword only inside this construct — everywhere else it is an ordinary identifier, exactly like send, so a variable may be called thing:

A thing called point has
  a number called x is 0,
  a number called y is 0.

a number called thing is 42.
Print thing.

A thing name may be a bare word (point) or a quoted multi-word name ('bounding box'), the same forms any identifier takes:

A thing called 'bounding box' has
  a number called width is 1,
  a number called height is 1.

a 'bounding box' called viewport.
Print viewport's width.

Field types in v1. A field may be number, float, boolean, time, or any previously defined thing (things nest to any depth — see Nesting). text, list, map, and buffer fields are deferred: they carry references and would reopen the aliasing question value copy semantics (below) is designed to avoid.

The article rule

a/an pairs with types and with values coming into being; the pairs with known identifiers. The rule is load-bearing in the surface syntax, so it is worth naming once:

  • A thing called point has ... — a type comes into being, so A.
  • a point called origin. — a value of that type comes into being, so a.
  • the point's 'placed at' in a member definition (To do the point's 'placed at') — point is a known identifier (the type, declared in the manifest), so the.
  • a point's 'placed at' with 1 and 0 — a new point comes into being from the maker, so a.

The same word, two articles, two meanings: the point's reads a known member; a point's calls a maker that brings a new point into being.

Declarations and field access

A thing name is a type noun everywhere the builtin ones are, so every declaration form works. All three lines below declare a point and give every field its declared default:

A thing called point has
  a number called x is 0,
  a number called y is 0.

a point called origin.
Set origin's x to 3.
Set origin's y to 4.
Print origin's x.
origin's y is origin's y add 1.
increment origin's x.
Print "origin sits at {origin's x}, {origin's y}".
If origin's x is greater than 3 then,
    Print "the origin moved right".

A field is an ordinary expression and an ordinary lvalue everywhere either is allowed — read, Set ... to, bare assignment, increment, decrement, format-string interpolation, and a comparison in a condition all appear above. Create declares with defaults too, and a quoted variable name is read and written through the same possessive:

A thing called point has
  a number called x is 0,
  a number called y is 0.

Create a point called 'the far corner'.
Print 'the far corner''s x.
Set 'the far corner''s y to 2.
Print 'the far corner''s y.

A field with no default takes its type's zero. A float and a boolean field carry defaults; a number field with none is 0:

A thing called 'water tank' has
  a float called 'depth in metres' is 1.5,
  a boolean called 'the pump is running' is true,
  a number called 'litres drained'.

a 'water tank' called cistern.
Print cistern's 'depth in metres'.
If cistern's 'the pump is running' then,
    Print "the pump is running".
Print cistern's 'litres drained'.

A thing declared inside a function is local to that function (its storage is the stack, not .bss):

A thing called point has
  a number called x is 0,
  a number called y is 0.

To 'plot a point'.
  a point called cursor.
  Set cursor's x to 9.
  increment cursor's y.
  Print "the cursor sits at {cursor's x}, {cursor's y}".

'plot a point'.

Nesting

A field may be a thing, so things nest to any depth. A nested thing contributes its own bytes inline, so a chained possessive is one sum of compile-time offsets — never a pointer chase — and the route's own 'route number' sits after the whole nested segment:

A thing called point has
  a number called x is 0,
  a number called y is 0.

A thing called segment has
  a point called start,
  a point called end.

A thing called route has
  a segment called leg,
  a number called 'route number'.

a route called commute.
Set commute's leg's start's x to 3.
Print commute's leg's start's x.
increment commute's leg's end's y.
Set commute's 'route number' to 66.
Print commute's 'route number'.

Defaults apply recursively: a field whose type is a thing takes that thing's own defaults, written into the nested bytes at declaration, so a stamp carried by a letter begins life with its own defaults even though nothing initialises it:

A thing called stamp has
  a number called 'day sent' is 25,
  a number called 'cost in pence' is 12.

A thing called letter has
  a stamp called posted,
  a number called 'weight in grams' is 2.

a letter called invitation.
Print invitation's posted's 'day sent'.
Print invitation's posted's 'cost in pence'.
Print invitation's 'weight in grams'.

A thing containing itself — directly, or through other things — has no finite size, so the definition that closes the cycle is a compile error naming the chain:

A thing called ouroboros has
  an ouroboros called tail.
(compile error: ouroboros contains ouroboros
   no finite size)

Things are acyclic by two mechanisms. Within one file, the defined-earlier ordering rule makes a cycle unconstructible: a field type must be a thing defined above the line, so a thing can never name itself or a thing defined below it. Across files reached by see, the analyzer's registry DFS proves the merged registry is acyclic. The DFS is load-bearing: it is what keeps the merged, multi-file registry acyclic, and it stands as defence-in-depth alongside the within-file ordering rule.

Value copy semantics

A thing is a value. Assignment copies the whole thing, and the copy shares nothing with the original — a thing's size is a compile-time constant, so a copy is a run of inline moves, no allocation and no pointer left aliased:

A thing called point has
  a number called x is 0,
  a number called y is 0.

a point called origin.
Set origin's x to 5.
a point called moved is origin.
Set moved's x to 9.
Print origin's x.
Print moved's x.

The three spellings of assignment — a declaration with an initialiser, a bare is, and Set ... to — are all assignment, so all three copy. A copy is deep by construction: a nested thing is just more bytes, so copying a letter carries its point along and neither half is shared:

A thing called point has
  a number called x is 0,
  a number called y is 0.

A thing called letter has
  a point called postbox,
  a number called 'weight in grams' is 2.

a letter called invitation.
Set invitation's postbox's x to 3.
a letter called reply is invitation.
Set reply's postbox's x to 7.
Print invitation's postbox's x.
Print reply's postbox's x.

The same is true across a call. A function receives a copy of a thing and hands one back by returning it; nudging the parameter cannot reach the caller's point, because the only way out is the Return, which copies into the caller's own storage:

A thing called point has
  a number called x is 0,
  a number called y is 0.

To nudged with a point called start.
  Set start's x to start's x add 1.
  Return a point, start.

a point called before.
The after is nudged of before.
Print before's x.
Print after's x.

The after is nudged of before. declares after from what the call returns, so a maker never has to have its type written twice. A whole nested thing read out of a segment is copied the same way:

A thing called point has
  a number called x is 0,
  a number called y is 0.

A thing called segment has
  a point called start,
  a point called end.

To nudged with a point called start.
  Set start's x to start's x add 1.
  Return a point, start.

a segment called span.
Set span's start's x to 40.
a point called 'the far end' is nudged of span's start.
Print 'the far end''s x.
Print span's start's x.

Because a thing is a whole shape, not a value, the things you cannot do to one are named rather than done to its first field. Assigning a single value to a whole thing, or stepping one with increment, is rejected with the field to write named instead:

A thing called point has
  a number called x is 0,
  a number called y is 0.

a point called origin.
Set origin to 5.
(compile error: 'origin' holds a whole point, so only a whole point can be copied into it
   A copy source is a variable holding a point, a field that holds one, or a call that returns one (plan 310 §5)
   To write one field instead, name it - point's fields are: x, y)
A thing called point has
  a number called x is 0,
  a number called y is 0.

a point called origin.
increment origin.
(compile error: 'origin' holds a whole point, not a value
   A whole thing is copied, passed, and returned whole (plan 310 §5)
   its fields are: x, y)

Writing one field is what those lines mean: Set origin's x to 5. A thing also cannot be interpolated into a text initializer (a text initializer is a different sink from Print) or compared with a single value; see Printing and Equality.

Printing a call's result directly needs a variable, because the result is a whole thing that must land in storage before it can be read:

A thing called point has
  a number called x is 0,
  a number called y is 0.

To nudged with a point called start.
  Set start's x to start's x add 1.
  Return a point, start.

a point called before.
Print nudged of before.
(compile error: A call to 'nudged' returns a whole point, which is not a value
   What a call returns is copied into a point)

The workaround is the inference form above — declare a scratch slot from the call, then print it:

A thing called point has
  a number called x is 0,
  a number called y is 0.

To nudged with a point called start.
  Set start's x to start's x add 1.
  Return a point, start.

a point called before.
The after is nudged of before.
Print after.

Printing

Print p. walks the fields in definition order and recurses into the things they hold, map-style. Every field name is baked into the emitted program, so nothing is read from a descriptor and nothing is allocated. A quoted field name prints in the quotes it is written with, and a function member takes no part — it is the type's API, not its state:

A thing called point has
  a function called 'placed at',
  a number called x is 0,
  a number called y is 0.

A thing called segment has
  a point called start,
  a point called end.

A thing called stamp has
  a number called 'day sent' is 25,
  a float called 'cost in pounds' is 1.5,
  a boolean called 'first class' is true.

To do the point's 'placed at', with a number called x and a number called y.
  a point called plotted.
  Set plotted's x to x.
  Set plotted's y to y.
  Return a point, plotted.

a point called origin.
Set origin's x to 5.
Print origin.

a segment called span.
Set span's end's y to 2.
Print span.
Print "the span runs {span}".

a stamp called posted.
Print posted.

The corner is a point's 'placed at' with 3 and 4.
Print corner.

A whole thing interpolates into a format string under Print (Print "the span runs {span}".), because Print is the sink that renders the fields. A text initializer is a different sink — it builds its bytes in a buffer — and interpolating a whole thing there is rejected, naming the field to interpolate instead:

A thing called point has
  a number called x is 0,
  a number called y is 0.

a point called origin.
a text called note is "the point is {origin}".
(compile error: 'origin' holds a whole point, which only `Print` can interpolate
   Interpolate a field instead - point's fields are: x, y)

Equality

is between two values of the same thing compares those same fields at the same depth; is not is its negation. Like printing, the comparison is written out by the compiler, so it recurses into nested things:

A thing called point has
  a function called 'placed at',
  a number called x is 0,
  a number called y is 0.

To do the point's 'placed at', with a number called x and a number called y.
  a point called plotted.
  Set plotted's x to x.
  Set plotted's y to y.
  Return a point, plotted.

a point called origin.
Set origin's x to 5.
a point called marker.
Set marker's x to 5.
If origin is marker then,
    Print "the marker is where the origin is".
Set marker's y to 9.
If origin is not marker then,
    Print "the marker has moved off the origin".

Two things of different types cannot be compared (origin is span is rejected — only two of the same thing have the same fields), and there is no ordering on a whole thing — origin is greater than marker is rejected, naming the field to compare instead:

A thing called point has
  a number called x is 0,
  a number called y is 0.

a point called origin.
a point called marker.
If origin is greater than marker then,
    Print "further along".
(compile error: 'origin' holds a whole point, which nothing puts in order
   Two things are compared for equality only (plan 310 §8)
   Compare a field instead - point's fields are: x, y)

The manifest

A thing's callable API is declared in one place, the manifest: each a function called <name> entry names a member. The member is then defined with To do the <type>'s <name>, and do is a keyword only in that position — everywhere else it is an ordinary identifier. The member definition uses the point's (a known identifier) — the article rule:

A thing called point has
  a function called 'placed at',
  a function called 'reflected through the origin',
  a number called x is 0,
  a number called y is 0.

To do the point's 'placed at', with a number called x and a number called y.
  a point called plotted.
  Set plotted's x to x.
  Set plotted's y to y.
  Return a point, plotted.

To do the point's 'reflected through the origin', with a point called original.
  a point called reflection.
  Set reflection's x to 0 subtract original's x.
  Set reflection's y to 0 subtract original's y.
  Return a point, reflection.

Function members take no storage, so layout, copy, printing, and equality see only the data fields.

Every declared member returns its owner. That is what gives the manifest a crisp meaning — it lists the functions that produce or transform the thing. A definition whose Return is not Return a point, is a compile error naming both lines:

A thing called point has
  a function called 'placed at',
  a number called x is 0.

A thing called 'grid square' has
  a number called column is 0.

To do the point's 'placed at', with a number called x.
  a 'grid square' called square.
  Set square's column to x.
  Return a 'grid square', square.
(compile error: A declared member returns its own thing: point's 'placed at' must return a point
   hands back a grid square
   A function that computes something else from a point is an ordinary function)

A function computing some other type from a point (like 'magnitude squared') is an ordinary global function, reached by the instance possessive, with no manifest entry at all. The owner-return check reads the body's Return lines, not the signature — so a member whose only Return sits inside an If is not wrongly rejected.

The manifest is checked both ways. A To do naming a member the manifest does not list errors at the definition, naming the entry to add:

A thing called point has
  a function called 'placed at',
  a number called x is 0,
  a number called y is 0.

To do the point's sparkle, with a point called original.
  Return a point, original.
(compile error: point does not declare sparkle - add `a function called sparkle` to the type
   Membership is declared in the thing's definition
   point declares: placed at)

and a declared member nothing defines errors at the type, where the promise was made:

A thing called point has
  a function called 'never written',
  a number called x is 0.

a point called origin.
Print origin's x.
(compile error: point declares 'never written' but nothing defines it
   To do the point's 'never written', with <parameters>.
   Return a point, <value>.)

A member is defined once: a second To do the point's 'placed at' errors at the second definition, naming the first.

The three call forms

Three ways to call, one rule each.

Free call — the function's own name, unchanged, in the global namespace. of, to, with, and on all introduce arguments:

A thing called point has
  a number called x is 0,
  a number called y is 0.

To 'magnitude squared' with a point called corner.
  a number called 'x squared' is corner's x multiply corner's x.
  a number called 'y squared' is corner's y multiply corner's y.
  Return a number, 'x squared' add 'y squared'.

a point called origin.
Set origin's x to 3.
Set origin's y to 4.
Print 'magnitude squared' of origin.

Instance possessivereceiver's 'member': sugar for 'member' of receiver. The receiver fills the function's first parameter; any further arguments follow the call preposition. A field always wins over a function of the same name, because the collision rule refuses that program rather than letting one shadow the other. A receiver is anything that names a whole thing, so a field holding one reads the same way:

A thing called point has
  a number called x is 0,
  a number called y is 0.

A thing called segment has
  a point called start,
  a point called end.

To 'magnitude squared' with a point called corner.
  a number called 'x squared' is corner's x multiply corner's x.
  a number called 'y squared' is corner's y multiply corner's y.
  Return a number, 'x squared' add 'y squared'.

To 'scaled by' with a point called corner and a number called factor.
  a point called scaled.
  Set scaled's x to corner's x multiply factor.
  Set scaled's y to corner's y multiply factor.
  Return a point, scaled.

a point called origin.
Set origin's x to 3.
Set origin's y to 4.
Print origin's 'magnitude squared'.
The 'tripled corner' is origin's 'scaled by' on 3.
Print 'tripled corner''s x.

a segment called 'the line'.
Set 'the line''s end's y to 12.
Print 'the line''s end's 'magnitude squared'.

Type possessivea <type>'s 'member': calls a member declared in the manifest. The article is a because a new thing comes into being. This is the only way to call a maker — a member whose first parameter is not the thing:

A thing called point has
  a function called 'placed at',
  a number called x is 0,
  a number called y is 0.

To do the point's 'placed at', with a number called x and a number called y.
  a point called plotted.
  Set plotted's x to x.
  Set plotted's y to y.
  Return a point, plotted.

The pin is a point's 'placed at' with 1 and 0.
Print pin's x.

A maker cannot be reached by the instance possessive (a receiver has nothing to fill), and the message says so rather than reporting the member as missing — the manifest does declare it:

A thing called point has
  a function called 'placed at',
  a number called x is 0.

To do the point's 'placed at', with a number called x.
  a point called plotted.
  Set plotted's x to x.
  Return a point, plotted.

a point called origin.
Print origin's 'placed at'.
(compile error: point declares 'placed at', but a receiver cannot reach it here
   'placed at' is a maker: its first parameter is not a point, so a receiver has nothing to fill
   Name the type instead: `a point's 'placed at' with <arguments>`)

A member whose first parameter is the thing gets both the type possessive and the instance possessive:

A thing called point has
  a function called 'placed at',
  a function called 'reflected through the origin',
  a number called x is 0,
  a number called y is 0.

To do the point's 'placed at', with a number called x and a number called y.
  a point called plotted.
  Set plotted's x to x.
  Set plotted's y to y.
  Return a point, plotted.

To do the point's 'reflected through the origin', with a point called original.
  a point called reflection.
  Set reflection's x to 0 subtract original's x.
  Set reflection's y to 0 subtract original's y.
  Return a point, reflection.

The pin is a point's 'placed at' with 1 and 0.
The opposite is pin's 'reflected through the origin'.
The 'opposite of the opposite' is a point's 'reflected through the origin' of opposite.
Print opposite's x.
Print 'opposite of the opposite''s x.

A member name belongs to its owner, not to the program: two things may each declare a 'placed at', and the two definitions compile under distinct internal names, so the same member name is fine on two different types:

A thing called point has
  a function called 'placed at',
  a number called x is 0,
  a number called y is 0.

A thing called 'grid square' has
  a function called 'placed at',
  a number called column is 0,
  a number called row is 0.

To do the point's 'placed at', with a number called x and a number called y.
  a point called plotted.
  Set plotted's x to x.
  Set plotted's y to y.
  Return a point, plotted.

To do the 'grid square''s 'placed at', with a number called column and a number called row.
  a 'grid square' called square.
  Set square's column to column.
  Set square's row to row.
  Return a 'grid square', square.

The 'marked square' is a 'grid square''s 'placed at' with 5 and 6.
Print 'marked square''s column.
Print 'marked square''s row.

do stays an ordinary identifier outside To do the <type>'s: To do. defines a function called do, and do. calls it.

One identifier space

Type names, variable names, and function names share a single global identifier namespace. This is what makes the point's unambiguous: there is only one point. Reusing a name is first-come-first-served, and the second definition errors at its own line, naming the first — whatever kind the first was:

a number called point is 0.
A thing called point has
  a number called x is 0.
(compile error: 'point' is already defined as a variable on line 1
   identifier space)

The same error names a function, a parameter, a loop variable, an inferred variable, or another thing, whichever came first. A thing's own fields and members live in a separate per-type member space (a type owns one), so point's x and segment's x do not collide. The collision rule there is first-come-first-served too: the second definition of any name in a type's member space — a field, a declared member, or a global function whose first parameter is that type — errors at its own line, pointing at the first:

A thing called point has
  a number called x is 0,
  a number called y is 0.

To x with a point called corner.
  Return a number, corner's y.
(compile error: point already has a field called 'x', so a function taking a point cannot be called 'x' too
   point is defined on line 1. A type owns one member space)

Definitions are top-level only

A thing is defined where a function is defined — at the top level. Its layout is fixed for the whole program and has no block scope, so a definition inside an If, a loop, or a function body is a compile error, and the message says to move it above the block:

To 'take a reading'.
  A thing called measurement has
    a number called degrees is 0.
  Print "taken".
(compile error: A thing is defined at the top level, like a function
   Canonical form: A thing called measurement has <fields>.
   Move the definition above the block it is written in)

The ordering rule is about the definition, not about an instance of it. A definition stands above every use of its name; an instance is an ordinary top-level variable, so it obeys the ordinary rule instead — "variables declared at top level are global and can be used inside functions", wherever on the page the declaration is written:

To 'show it'.
  Print origin's x.      (reads the global declared below)

a point called origin.

Cross-file definitions

A thing defined in one file is usable from another via see (the definition is parsed into the program the way a function is). The whole surface crosses the boundary: the type noun in a declaration, a field read and write, the manifest member reached by the type possessive, and a global function taking the thing reached by the instance possessive. The seen file arrives where the see is written, so the same defined-earlier rule that orders one file orders the pair — every use below stands after the definition it names.

(./include/geometry.vox — the definition and the maker travel together)
A thing called point has
  a function called 'placed at',
  a number called x is 0,
  a number called y is 0.

To do the point's 'placed at', with a number called x and a number called y.
  a point called plotted.
  Set plotted's x to x.
  Set plotted's y to y.
  Return a point, plotted.

(An ordinary global function taking a point first, so the including file
 can reach it through the instance possessive as well as by name.)
To 'shifted east' with a point called start.
  Set start's x to start's x add 1.
  Return a point, start.
(Consumer file — sees the definition above.)
see "./include/geometry.vox".

a point called origin.
Set origin's x to 11.
Print origin's x.

The corner is a point's 'placed at' with 3 and 4.
Print corner's x.
Print corner's y.

The 'shifted corner' is corner's 'shifted east'.
Print 'shifted corner''s x.

A type name is one identifier across the whole compilation: defining the same thing in two files reached by see errors at the second definition, naming the other file. The diagnostic reads 'point' is already defined as a thing on line 4, then names the file that defined it first (include/point_defined_elsewhere.vox) and the rule (identifier space).

A see of a file that cannot be read is an error.

.lib export of a thing is not yet supported

A .lib interface file names types by noun, and no noun spells a user-defined thing, so an exported signature that takes or returns a thing cannot be written. Ordinary compilation is unaffected; an exported library function whose signature mentions a thing is refused with a message naming the field and the canonical workaround — pass the thing's fields across the boundary instead. A library that exports a function To 'nudged east' with a point called start. — taking a point and, in the same case, returning one — is refused when compiled with --shared:

takes a point ('start'), which a library interface cannot describe yet returns a point, which a library interface cannot describe yet A thing is a layout private to one compilation

The same source compiles fine as an ordinary program; the refusal fires only at the library interface, because the interface has no noun for a user-defined thing. The diagnostic names each crossing field and points at the workaround — pass start's x and start's y as separate values.

Sentence consumption and multi-line definitions

A thing definition is a new place the sentence consumption rules bite. Its entries are comma-separated, and the construct closes on a period or a blank line, the same termination rules every other construct follows: a period closes the entry list, and a blank line force-closes it (along with anything else still open). Indenting the entries is conventional but not required — the commas and the terminator carry the structure.

Definition diagnostics

The definition construct creates a family of sentences that are never valid Vox. Each gets a targeted error stating the intent it recognises and naming the canonical form.

Create a thing called point.
(compile error: A thing is defined, not created as a variable
   Canonical form: A thing called point has <fields>.)
A thing called point is 5.
(compile error: 'is' declares a variable; a thing definition uses 'has'
   Canonical form: A thing called point has <fields>.)
A thing called point has.
(compile error: A thing needs at least one field
   Canonical form: A thing called point has)

A definition listing only manifest entries describes a zero-byte thing, so v1 requires at least one data field:

A thing called point has
  a function called 'from polar'.
(compile error: A thing needs at least one field
   `a function called <name>` declares callable API, not storage.)

A field default must be a literal of the field's own type — a computed value belongs in a function that returns the thing:

A thing called point has
  a number called x is 1.5.
(compile error: Field 'x' of thing 'point' is a number, but its default is a float
   literal of the field's own type)

Declaring with an unknown type name keeps the existing unknown-type error, extended to suggest near-miss user-defined type names alongside the builtins:

A thing called point has
  a number called x is 0,
  a number called y is 0.

a poimt called origin.
(compile error: Unknown type 'poimt'
   did you mean
   point)

Type predicates and the runtime tag

User-defined things are not in the runtime tag system in v1. The type nouns is a <type> recognises are the builtins (number, text, decimal, boolean, list, map); there is no is a point yet, and a list or map of user things, or a value holding one, is likewise deferred. Things live in the compile-time type table, not the runtime tag.

Expressions

Literals

TypeExample
Integer42, 0, -5
Float3.14, -2.5, 0.0
String"Hello, World!"
Booleantrue, false
Hexadecimal0xFF, 0xDEADBEEF
Binary0b10110100, 0b1111
Character'A', '!'

Note: Float literals are recognized by the presence of a decimal point. Floats and integers can be mixed in arithmetic expressions.

Note: Arithmetic operates on numbers (booleans count as 0/1). Text, buffers, and lists must be cast with as a number or as a float before they can be used in arithmetic - using them directly is a compile error, since they hold pointers rather than numeric values.

Hex and Binary:

  • Hexadecimal literals use 0x prefix: 0xFF equals 255
  • Binary literals use 0b prefix: 0b1010 equals 10
  • Character literals use single quotes: 'A' equals 65

Variable Reference

  • the x - references the variable x
  • the number - references loop iterator (inside for each)
  • x - direct identifier reference

Arithmetic

the x add 5
y subtract 3
the lhs multiply rhs
total divide 2
x modulo 3
{x add y} multiply z
{fibonacci of n subtract 1} add {fibonacci of n subtract 2}

Note: the is optional before variable names in expressions.

For complex arithmetic subexpressions, use curly braces {...} to group each subexpression. A cast (as a <type>) binds tighter than arithmetic and applies to the expression immediately to its left, so s as a number add 1 casts s and then adds 1. To cast a whole arithmetic expression, brace it: {a add b} as a number. Comma-separated arithmetic continuation (for example ..., add ...) is not valid syntax.

Comparisons

the x is greater than 5
y is less than 10
lhs is equal to rhs
x is 0

Note: the is optional before variable names in comparisons.

Property Checks

the x is even
the y is odd
the z is positive
the n is negative
the value is zero
the list is empty

Logical Operators

<condition> and <condition>    (true if both conditions are true)
<condition> or <condition>     (true if either condition is true)
not <condition>                (true if condition is false)

not takes the whole condition after it, exactly as the fence above says and exactly as English does: If not heat is limit then, reads "if it is not the case that heat is limit", never "if the negation of heat is limit". So not binds looser than every comparison and property check, and tighter than and and ornot heat is 4 and limit is 6 is {not (heat is 4)} and (limit is 6). A not in front of a boolean is that same rule with the shortest condition: If not door_open then,.

A not always answers a boolean, whatever it is applied to: not 5 and not greeting are booleans, not a number and a text. On a text, list, map or buffer, not tests the value's pointer — which a declared variable always has — so it answers false whether or not the collection holds anything. Ask about contents with is empty (see Property Checks above), never with not.

Plural Comparisons with are

Test multiple variables against the same value using comma-separated subjects:

if x, y, and z are true
if a, b, and c are not false
if 'door open', lift_moving, and lift_full are not true

Expansion:

if x, y, and z are true

expands internally to:

if x is true and y is true and z is true

Rules:

  • Subjects are separated by commas
  • The word and before the last subject is optional but recommended for natural language readability
  • The predicate after are applies to ALL subjects
  • are not negates the comparison for all subjects

Type Casting

Convert values between types using the as or in keywords.

Syntax:

<value> as a <type>
<value> as <type>
<value> in <unit>

Basic Conversions:

FromToSyntaxResult
floatnumber3.14 as a number3 (truncated)
numberfloat42 as a float42.0
numbertext25 as text"25"
textnumber"123" as a number123
floattext3.14 as text"3.14"
textfloat"3.14" as a float3.14
booleannumbertrue as a number1
booleannumberfalse as a number0
numberboolean0 as a booleanfalse
numberboolean42 as a booleantrue
booleantexttrue as text"true"
textboolean"true" as a booleantrue
buffertextdata as texta copy of the buffer's bytes

A text made from a buffer is an independent copy, not a window onto the buffer. a text called line is data as text. reads the buffer's current bytes once and keeps its own copy, so clearing, refilling, or resizing data afterwards leaves line exactly as it was — the same promise format strings make (see "Format Strings as Values"). This matters because resizing frees the buffer's old allocation: without the copy, reading such a text would be reading freed memory.

The cast is optional for this one conversion. Every spelling that puts a buffer into a slot that holds text means the same thing and makes the same copy — a text called line is data., Set line to data., the line is data., a text parameter given a buffer argument, and Return a text, data. — as do data as text and "{data}". Writing the cast is still good style where the type change is worth pointing at, but leaving it out never changes what the sentence does. This does not loosen type immutability: line is text before the write and text after it, and every other mismatched write is still the compile error described under "Type Immutability".

A value slot is one of those slots. A buffer written into a value — by declaration, by Set, by the ... is, as a value argument, or by Return a value — arrives as text and reports Text (dynamic), carrying the same independent copy of the buffer's bytes. A value never holds a buffer as a buffer; there is no Buffer (dynamic) tag.

A float read from text is the same double as the literal. "0.88" as a float and the literal 0.88 are one value, and comparing them with is finds them equal: the runtime reads a decimal exactly the way the compiler reads one written in the source. The guarantee covers up to eighteen significant digits with the point up to twenty-two places away from them - wider than a float can tell apart - and a longer decimal is read as the nearest float those eighteen digits describe. This is what lets a number read from a file, an argument or an environment variable be compared against a literal in the same program.

Radix (Base) Conversions:

Text-to-number casting isn't limited to base 10. A radix word can be inserted right before number to parse in a different base:

SyntaxBaseExampleResult
as a number10 (default)"42" as a number42
as a hex number / as a hexadecimal number16"ff" as a hex number255
as an octal number8"17" as an octal number15
as a binary number2"1010" as a binary number10
as a base N number (spaced)any 2-36"z9a" as a base 36 number45694
as a baseN number (fused)any 2-36"6543" as a base7 number2334

Any base from 2 through 36 is supported, not just the aliased ones (hex/octal/binary) - digits above 9 use letters a-z (case- insensitive), so base 36 is the practical maximum for a single- character-per-digit representation.

(Hex string to number)
a text called hexstr is "3fa2c1e4".
a number called n is hexstr as a hex number.

(Arbitrary base, fused or spaced form - both work)
a text called s is "6543".
a number called n2 is s as a base7 number.
a number called n3 is s as a base 7 number.

(Negative numbers and uppercase hex digits both work)
a text called neg is "-1a".
a number called n4 is neg as a hex number.   (-26)
a text called upper is "FF".
a number called n5 is upper as a hex number. (255)

Like the base-10 case, parsing stops at the first character invalid for that base rather than raising an error - "12g5" as a hex number gives 18 (stops at g), and a string that's invalid from its very first character (e.g. "abc" as a base5 number, since a's value of 10 is too big for base 5) gives 0.

Examples:

(Float to number - truncates)
a float called pi is 3.14159.
a number called 'pi truncated' is pi as a number.

(Number to text)
a number called age is 25.
a text called agestr is the age as text.

(Text to number - parsing)
a text called userinput is "123".
a number called parsed is the userinput as a number.

(Boolean to number)
a boolean called done is true.
a number called 'done num' is the done as a number.

(Inline casting)
Print 3.14159 as a number.

The in Keyword:

The in keyword reads more naturally for timer duration casts. It applies to a timer's duration or elapsed, not to a plain number:

(Duration from timer)
Print the timer's duration in seconds.
Print the timer's elapsed in milliseconds.

in only works on a timer's duration/elapsed (it lowers to a duration cast); <number> in <unit> on a plain number is not valid syntax. To convert a plain number of milliseconds to seconds, divide: the millis divide 1000.

Formatted Output:

Numbers can be converted to padded text for display formatting with the zero-pad format specifier:

(Pad to 2 digits - for times like 09:05)
a number called h is 9.
a text called hpadded is "{h:02}".
Print the hpadded.  (prints "09")

Casting Rules:

  • as a <type> and as <type> are equivalent (article is optional)
  • A cast binds tighter than arithmetic and applies to the expression immediately to its left: n as a number add 1 is (n as a number) add 1. Brace to cast a whole expression: {a add b} as a number
  • Float to number truncates (does not round)
  • To round: add 0.5 before casting ({3.7 add 0.5} as a number4)
  • Text, buffers, and lists cannot be used directly in arithmetic; cast them with as a number / as a float first
  • Text to number fails if text is not a valid number (sets error flag)
  • Text to number in a non-default base (as a hex/octal/binary/base N number) stops parsing at the first character invalid for that base, rather than failing outright - it does not set the error flag
  • Zero is false, any non-zero number is true
  • in keyword is for timer duration/elapsed casts (see above)

Control Flow

If Statement

If <condition> then, <statement>.

With else:

If <condition> then, <statement>. Otherwise, <statement>.

With else-if:

If <condition> then, <statement>. But if <condition> then, <statement>. Otherwise, <statement>.

Sentence consumption rule (important):

  • Each then, / but if ... then, / otherwise, branch consumes actions until the sentence ends.
  • Separate multiple actions in a branch with commas.
  • Use a period to end the full if sentence.
  • A period before but if/otherwise is treated as part of the same if-chain when the chain continues.
If ready then, print "a", print "b", print "c".

Alternative keywords:

  • When can replace If
  • Else can replace Otherwise

While Loop

While <condition>, <statements>.

Single-line example:

While the counter is less than 10, print the counter, increment the counter.

Multi-action loops are comma-separated actions within one sentence:

While x is less than 5, print x, increment x, print "looping".

Loops inside functions work naturally:

To sum of a number called n.
  a number called total is 0.
  a number called i is 1.
  While i is less than or equal to n, total is total add i, i is i add 1.
  Return a number, total.

For Each Loop

Range-based:

For each number from <start> to <end>, <statement>.

Example:

For each number from 1 to 10, print the number.

Inside the loop:

  • the number refers to the current iteration value

List-based:

For each <variable> in <list>, <statement>.

Example:

a list called nums is [1, 2, 3].
For each n in nums, print the n.

Repeat

Run a body a fixed number of times.

Repeat <count> times, <statements>.

Single-line example:

Repeat 3 times, print "hello".

Multi-action loops are comma-separated actions within one sentence, exactly like While:

Repeat 2 times, print "a", print "b".

This prints a, b, a, b — two actions per iteration, two iterations.

Termination. Repeat closes by the same rules as While and For each: a period ends the body (and closes the construct — rule 1), and a blank line force-closes it (rule 2). The statements after a closing period belong to the surrounding scope, not the loop:

Repeat 2 times, print "r".
Print "after".

r r after

Because a period closes the construct, periods stack: write one period per level you want to close, so a Repeat nested in another loop takes two periods to close both (see Closing more than one level):

For each n from 1 to 2,
    Repeat 2 times, print "r"..
Print "after".

r r r r after

Loop Control

Break.
Continue.

Program Termination

Immediately exit the program with an exit code:

Exit <code>.

Examples:

Exit 0.                              (Success)
Exit 1.                              (General error)

If arguments's empty then,
    Print "Usage: ./program <file>".
    Exit 1.

Notes:

  • Exit code defaults to 0 if not specified
  • All resources are automatically cleaned up before exit
  • Alternative keywords: quit, terminate

Increment/Decrement

Increment the counter.
Decrement the value.

Lists and Collections

List Literals

Create lists with square brackets containing comma-separated values:

a list called nums is [1, 2, 3].
a list called names is ["Alice", "Bob", "Charlie"].
a list called mixed is [1, "two", 3].
a list called emptylist is [].

Key points:

  • Lists are 1-indexed (like natural language: "the first element", "the second element")
  • Lists can contain mixed types
  • Empty lists [] are allowed
  • Lists are allocated on the heap with automatic memory management

Mixed-Type Lists

A list may freely hold numbers, texts, decimals, and booleans together. The author never declares this - the compiler resolves it. Lists it can prove homogeneous keep a statically-typed fast path; lists with mixed elements carry a small per-slot type tag at runtime, so every element prints and reads back as what it is:

a list called m is [1, "two", 3.5, yes].
For each item in m, print item.
(prints: 1, two, 3.5, 1)

Appending, set element, element N of, first/last, iteration, and {...} format interpolation all respect each element's actual type. Booleans print as 1/0, matching homogeneous boolean lists.

The compiler earns the homogeneous fast path by proof, not assumption. A value whose type it cannot statically prove widens the list to mixed, so reads dispatch on each slot's runtime tag rather than on one assumed type. A value is the everyday case: its type travels with its payload as a runtime tag, so the slot is written with the type the payload actually has, whatever that turns out to be.

a value called tally is 5.
a list called items is [].
append "hello" to items.
append tally to items.
print element 1 of items.   (prints: hello)
print element 2 of items.   (prints: 5)

A function result whose return type is declared (e.g. Return a text, "hi".) is statically known, so it is tagged with that type at the write and widens the list only because its type differs from the other elements.

A function whose return type is not declared is the one thing a slot cannot be written from. Nothing proves what the result is, and nothing carries a tag for it either, so the write would have to guess — and a returned text stored under a guessed number tag reads back as the raw address of its bytes, which is the silent wrong answer the identifier/literal split exists to prevent (see Names and strings). So it is refused at compile time rather than guessed, and the error names both ways out: declare the return type, or assign the result to a declared variable and append that.

To five with a number called x. Return x add 1.
a list called items is [].
append five of 4 to items.   (compile error: 'five' has no declared return type)

The same rule holds everywhere else a result lands with no type of its own — print <call>, a {...} interpolation, a list literal slot, set element, a map value, and a value declaration. A position that does supply a type is unaffected: a declared variable, a later assignment to one, and an argument landing on a declared parameter all read a result back as what the declaration says it is.

Full runtime tag propagation, which would let an opaque call carry its own tag the way a value does, is stage 1d — see docs/COLLECTIONS_ROADMAP.md for the roadmap.

Nested Lists

A list element may itself be a list. A nested list prints recursively with brackets, and the same per-slot tag machinery tracks it — a list value in a slot carries the list tag (4), so a mixed list like [1, [2, 3], "four"] prints exactly as written, and a homogeneous list-of-lists like [[1, 2], [3, 4]] keeps the statically-typed fast path (it is not mixed):

a list called nested is [1, [2, 3], "four"].
print nested.                       (prints: [1, [2, 3], "four"])
print element 2 of nested.          (prints: [2, 3])

a list called deep is [1, [2, [3, 4]], 5].
print element 2 of element 2 of deep.   (prints: [3, 4])

element N of, first/last, iteration, and whole-list print all yield a usable child list, so an extracted child behaves as a list — its length, its own element N of, and a For each over it all work:

a list called inner is element 2 of [1, [2, 3], "four"].
print inner's length.        (prints: 2)
For each y in inner, print y.   (prints: 2, then 3)

The is a list predicate recognises a nested-list element (runtime tag 4) and folds to true on a statically-typed list variable, like the other predicates:

For each item in [1, [2, 3], "x"],
  if item is a list then, print "L". otherwise print "s".
(prints: s, L, s)

Printing is recursive and cycle-safe: a list that contains itself (for example a list called x is []. append x to x.) would recurse forever, so printing is capped at a depth of 64. When the limit is hit the over-deep subtree prints as ..., the error flag is set, and printing unwinds safely instead of overflowing the stack. Use on error to react:

a list called x is [].
append x to x.
print x.
on error print "cyclic".
(prints: [[...]] then cyclic — abbreviated: 64 opening brackets, then
 `...`, then 64 closing brackets, then `cyclic`)

One limitation remains for this stage. Extracting a child with element N of yields a reference to the child list, not a copy: if the parent is later grown by appending enough elements to force a reallocation, a child extracted before that reallocation may point at freed memory. Extract a child after the parent has finished growing, or copy it element-by-element. See docs/COLLECTIONS_ROADMAP.md for the roadmap.

Maps

A map is a key/value collection — a JSON object. Keys are text; values may be any type (number, text, decimal, boolean, list, or another map). A map literal uses braces with "key": value pairs, and an empty map is {}:

a map called person is {"name": "Ada", "age": 36}.
a map called emptymap is {}.
print person.            (prints: {"name": "Ada", "age": 36})
print emptymap.          (prints: {})

Read a value by key with map's "key" (the key is a text literal; a quoted key with {...} interpolation builds a dynamic key). The value carries its runtime tag, so a text prints as text and a number as a number:

print person's "name".   (prints: Ada)
print person's "age".    (prints: 36)

Insert or replace an entry with Set map's "key" to value (mirroring Set element N of list to …). The map may reallocate on growth, so the returned pointer is stored back into the variable automatically - including when the variable is a map parameter, in which case the caller's map is what grows (see A collection parameter is the caller's collection):

set person's "age" to 37.
print person's "age".    (prints: 37)
print person's length.   (prints: 2 — replace, not insert)

The properties length (live entry count) and empty (true when zero entries) work as for lists. keys and values each yield a fresh list, in insertion order, for iteration:

for each key in person's keys, print key.   (prints: name, then age)
for each v in person's values, print v.     (prints: Ada, then 37)

A missing key does not crash: the lookup sets the error flag, so an on error handler can react, and yields a value the destination can hold. Where the compiler can prove the key is absent — a map literal it can see all of — the read is the number 0 whatever the map's values are, so read it into a number (a float or a boolean holds 0 too), and a text, list or map destination is refused with a diagnostic naming the key. Where it cannot prove it — a dynamic key, or a map an Append, a Set, an alias or a call can reach — the read yields the destination's default value from the table under Two Canonical Forms: 0 for a number, the empty text for a text, [] for a list, {} for a map, so no read ever dereferences a null pointer. Note this is deliberately not the same as a key that holds nothing — "no such key" stays distinguishable from "the key is set to nothing":

print person's "nope".    (prints: 0)
on error print "missing". (prints: missing)

A map value may be a list or another map, and printing is recursive: _map_print renders {"key": value, …} and shares the same 64-deep _print_depth budget as _list_print, so a mixed map/list tree is cycle-safe. A self-referential map (set m's "self" to m.) prints 64 levels deep, then ..., sets the error flag, and unwinds safely.

The is a map predicate recognises a map (runtime tag 5): it folds to true on a statically-typed map variable and compares the tag at run time on a mixed value. A map also rides the value ABI (see Values): a map passed to a value parameter or returned from a value function carries its tag (5) alongside the payload, so it round-trips through functions intact.

A map may also be an element of a list ([{"a": 1}, {"b": 2}]) — the slot carries the map tag (5), so is a map fires on a For each loop variable over such a list. The loop variable itself is deliberately untyped, though, and reading a key with 's "key" is a static check, so entry's "tag" inside the loop is a compile error ("Map access target must be a map"). To read a key, loop over the positions and declare the element:

a list called holder is [{"tag": 1}, {"tag": 2}].
For each position from 1 to holder's length,
  a map called entry is element position of holder,
  print entry's "tag".
(prints: 1, then 2)

Two limitations remain for this stage: keys are text only (a non-text key is rejected with "Map keys must be text"), and there is no entry deletion. See docs/COLLECTIONS_ROADMAP.md.

Type Predicates

You can ask what type a value actually holds and branch on it. The predicate is a <type-noun> compares the value's runtime type tag, so it works on a mixed-list element whose type is only known at run time:

a list called m is [1, "two", 3.5, yes].
For each item in m,
  if item is a text, print "text: {item}",
  otherwise if item is a decimal, print "decimal: {item}",
  otherwise if item is a boolean, print "boolean: {item}",
  otherwise print "number: {item}".
(prints: number: 1 / text: two / decimal: 3.5 / boolean: 1)

The type nouns are number, text, decimal, boolean, list, and map. The declaration synonyms also work (integer→number, string→text, float/real→decimal, bool→boolean, dictionary→map). Negate with is not a:

if item is not a number, print "not a number".

is a boolean and is a number are distinct even though both print as numbers: a boolean carries tag 3, a number tag 0, and the predicate reads that tag. On a statically-typed value the predicate folds at compile time — if x is a number for a declared a number called x costs nothing and is always true — so the sentence is legal on any value, not just mixed ones.

This is the guard idiom that makes mixed lists programmable — with one constraint worth stating plainly. The predicate reads the runtime tag; it does not narrow the static type. Arithmetic still dispatches statically, so operating on the tested value itself is refused inside the guard exactly as it is outside it ("Cannot use a value item in arithmetic"). Guarding therefore means getting the element into a declared variable, which a For each loop variable can never be — loop over the positions instead:

a list called mixedbag is [1, "two", 3.5].
For each position from 1 to mixedbag's length,
  if element position of mixedbag is a number,
    a number called got is element position of mixedbag,
    print got add 1.
  otherwise print "guarded away".
(prints: 2, then guarded away, then guarded away — 3.5 is a decimal,
 not a number, so `is a number` is false for it)

(Automatic guarding is a later decision; see the roadmap.) The cast expression is not a way round this: item as a number on a dynamically-tagged element is rejected for the same reason ("casting a dynamically-tagged value is not currently supported by the compiler — a known gap"). <value> as a <type> converts a statically-typed value; see Type Casting. Use the idiom above instead.

A predicate result is itself a boolean value, so you can store one in a list — append item is a number to flags — and each stored slot carries the boolean tag, so a later is a boolean recognises it.

User-defined things are not in this tag system in v1. The nouns above are the builtins; there is no is a point for a thing you define, and a list or map of user things, or a value holding one, is likewise deferred. A thing lives in the compile-time type table, not the runtime tag — see Things.

Dynamic Values (value)

The value type is a declared dynamic type that carries its runtime tag alongside its payload across the call, so a single function can accept "whatever this slot holds" and ask is a ... inside to find out which.

Declare a value parameter with with a value called x, return one with Return a value, <expr>, and a value local with a value called r:

To describe with a value called item.
  If item is a number, print "number".
  Otherwise if item is a text, print "text".
  Otherwise print "decimal".

a list called m is [1, "two", 3.5].
For each item in m,
  describe of item.
(prints: number / text / decimal)

Inside the callee, item is a value (a tagged slot): the is a ... predicates read its tag, printing dispatches on it, and you can forward it or append it back into a list with the tag preserved. A function returning a value carries its tag back out, so this round-trips:

To echo with a value called v. Return a value, v.

a list called data is [1, "two", 3.5].
a list called out is [].
For each item in data,
  append echo of item to out.

After the loop, out holds [1, "two", 3.5] with the original tags intact — the value return brought each tag back out, and the append forwarded it.

value is not a reserved word. It is recognized only where a type is expected: a parameter type, a return type, or directly before called in a value called x. Everywhere else it is an ordinary identifier, so a value is 5. still declares a variable named value.

A value local keeps its tag through reassignment, so set r to 7. retags it as a number:

To echo with a value called v. Return a value, v.

a value called r is echo of "hello".
print r.                      (prints: hello)
set r to 7.
If r is a number, print "now a number".

Bare arithmetic on a value is still rejected. Because a value might hold a string or a decimal, the compiler refuses to use it directly in arithmetic:

To bump with a value called v. Return a number, v add 1.
(compile error: Cannot use a value v in arithmetic: its type is only known
 at runtime, and arithmetic on a dynamically-tagged value is not currently
 supported.)

A value can be retyped in place. This is the exception named in Type Immutability above: a statically-typed variable's type is fixed forever, but value is deliberately not one. The statement <valuevar> is a <type>. reads the variable's runtime tag, performs the conversion that the corresponding static cast would use, and stores the result back into the same variable with the new tag. This works for number, float/decimal, text, and boolean targets:

a value called numstr is "357".
numstr is a number.
print numstr add 1.           (prints: 358)

The explicit as cast is not an alternative here: numstr as number is a compile error on a value, because a cast needs its source type at compile time and a value only knows its type at runtime — the in-place retype is how a value is converted.

The same phrase in condition position keeps its old meaning: If numstr is a number then, ... is still a type predicate that tests the runtime tag and returns a boolean. Position — statement versus condition — is what distinguishes a cast from a predicate:

a value called numstr is "357".
if numstr is a number then, print "num".
otherwise, print "not num".
(prints: not num)

After a successful in-place retype, the variable is tracked with the new type for the rest of its lifetime, so arithmetic and further casts behave accordingly. Retyping to the type it already holds is a no-op.

Failed conversions set _last_error and leave the variable as 0. A text that cannot be parsed as a number, for instance, results in 0 and raises the error flag so On error can catch it:

a value called bad is "abc".
bad is a number.
on error print "cast failed".
print bad.
(prints: cast failed / 0)

Inspecting a value's current type. The universal type property reads the variable's runtime tag and returns a text description such as Text (dynamic), Number (dynamic), Float (dynamic), Boolean (dynamic), List (dynamic), Map (dynamic), or Nothing (dynamic). Because it reads the tag, the reported type changes with reassignment:

a value called v is "hello".
print v's type.          (prints: Text (dynamic))
set v to 42.
print v's type.          (prints: Number (dynamic))

This is a display helper for debugging and logging; type tests still belong in the is a <type> predicate.

The list is the whole list: those seven are every tag a value can carry. A buffer put into a value is converted to text on the way in (see "Type Casting"), so it reads back as Text (dynamic).

Retyping a statically-typed variable is a compile error. n is a text. is only valid when n was declared as a value; for a number variable the compiler reports the actual declared type and points at the explicit cast (a text called t is n as text.) as the correct rewrite.

Recursion with value works. A value parameter threads its tag through every frame, so a recursive walker over mixed data classifies correctly at any depth. value parameters compose: a value passed straight to another value function round-trips its tag.

Conditional value returns work. A function whose only returns sit inside an If/Otherwise — the factorial pattern, with no Return on the To line — carries its declared return type just as the single-expression form does, and each branch hands back its own runtime tag:

To score with a value called v.
  If v is a number, return a value, v.
  Otherwise, return a value, 99.

print score of 7.          (prints: 7)
print score of "hello".    (prints: 99)

The same is true of a conditional return of any declared type: Return a text, "big". inside a branch makes the function a text-returning one. If no branch fires and the function falls off its end, it hands back the empty value of its declared type — empty text, zero, or a value tagged as the number 0.

One limitation to know. A function whose branches declare different types — Return a text in one and Return a number in the other — has no single type for its To line to promise, so it declares none and the caller reads the result as a number. Declare the same type in every branch, or return a value, which is exactly the type for a result whose shape depends on the branch taken. Conditional value parameters (the factorial pattern with a void return) work as they always have. The internal ABI that carries the tag is documented in docs/abi_value.md; the roadmap context is in docs/COLLECTIONS_ROADMAP.md (stage 1d).

Nothing (the absent value)

nothing is the value that means "no value here" — the equivalent of null in other languages. It can sit in a list slot, a map value, or a value parameter or return, and it prints as the word nothing:

a list called L is [1, nothing, "x"].
print L.
(prints: [1, nothing, "x"])

a map called m is {"found": 4, "absent": nothing}.
print m.
(prints: {"found": 4, "absent": nothing})

null and nil are accepted spellings of the same literal; all three produce the identical value. nothing is a reserved word, so it cannot be used as a variable name.

Test for it with is nothing, which is an equality (like is true), not a type predicate — there is no is a nothing:

If m's "absent" is nothing, print "no value stored".
If m's "found" is not nothing, print "has a value".

nothing is not zero. This is the distinction that matters most:

If 0 is nothing, print "never printed".

0 is nothing is false, and nothing is 0 is false too. They are different values, and is nothing compares the runtime type tag rather than the stored number, so the two never collide.

A missing map key is an error, not nothing. Reading a key that was never set sets the error flag; it does not silently hand back nothing. So "the key is absent" and "the key holds nothing" stay distinguishable:

a map called m is {"k": nothing}.
If m's "k" is nothing, print "k is present and holds nothing".
a number called x is m's "never_set".
on error print "never_set is absent".

Arithmetic on nothing is refused, not treated as 0. Writing it literally is a compile error:

a number called n is nothing add 1.
(compile error: Cannot use nothing in arithmetic; check it with
 'is nothing' first.)

When a value only turns out to be nothing at run time — read out of a map or a mixed list — the compiler cannot catch it, so the operation sets the error flag instead:

a map called m is {"absent": nothing}.
a number called bad is m's "absent" add 1.
on error print "cannot do arithmetic on nothing".

The reason for both is that the stored payload of nothing really is 0. Left unchecked, total add missing_field would quietly evaluate to total — a wrong answer that looks completely plausible. Guard with a predicate first, exactly as you would for a mixed element:

If m's "absent" is not nothing, set total to total add m's "absent".

Comparisons are not arithmetic, so is nothing, is not nothing, and ordinary equality keep working on a nothing without raising the flag.

Printing a List

Printing a list variable directly renders its contents rather than its heap address:

a list called nums is [1, 2, 3].
a list called m is [1, "two", 3.5, yes].
print nums.               (prints: [1, 2, 3])
print m.                  (prints: [1, "two", 3.5, 1])
print "list: {nums}".     (prints: list: [1, 2, 3])

Elements are separated by , and wrapped in [ ]. Each element renders by its own type, not the list's: text elements are quoted (so ["1"] is distinguishable from [1]), booleans as 1/0, floats and numbers as usual. Empty lists print []. A nested list element renders recursively with the same rules (see Nested Lists above), so [1, [2, 3], "four"] prints with inner brackets intact. A map element (or a whole map) renders as {"key": value, …} via _map_print (see Maps above). The same rendering appears inside {...} format interpolation, in both its forms and in every sink: the variable form (print "{xs}") and the expression form (print "{element 2 of xs}") each dispatch on the element's runtime tag, so an element renders in a hole exactly as it does printed as a statement.

List Properties

Access list properties using the 's syntax:

a list called items is [10, 20, 30].

print items's length.      (prints 3)
print items's size.        (same as length)
print items's first.       (prints 10)
print items's last.        (prints 30)
print items's empty.       (prints 0)
PropertyDescriptionType
lengthNumber of items in the listNumber
sizeSame as lengthNumber
emptyWhether the list has no itemsBoolean
firstThe first item in the listItem
lastThe last item in the listItem

List Element Access

Access elements by index (1-indexed):

a list called nums is [10, 20, 30].

Print element 1 of nums.   (prints 10)
Print element 2 of nums.   (prints 20)
Print nums's first.        (prints 10)
Print nums's last.         (prints 30)

(Using variable index)
a number called i is 2.
Print element i of nums.   (prints 20)

Bounds checking:

  • Out-of-bounds access sets an error flag. Where the compiler can prove the index is past the end, it returns the number 0 whatever the list's elements are, so read it into a number; where it cannot, it returns the destination's default value from the table under Two Canonical Forms0 for a number, the empty text for a text, [] for a list, {} for a map
  • Errors can be caught with On error
a list called items is [1, 2, 3].
a number called bad is element 100 of items.
On error print "Cannot access element 100 - out of bounds!".

Appending to Lists

Add elements to the end of a list using the append keyword:

a list called nums is [1, 2, 3].
append 4 to nums.
append 5 to nums.
print nums's length.       (prints 5)

append is overloaded by destination type:

  • append <value> to <list> appends one list element.
  • append <source_buffer> to <destination_buffer> appends source bytes to destination buffer bytes.

Use copy <source_buffer> to <destination_buffer> to replace destination buffer contents. Use clear <buffer> to reset a buffer to empty while preserving capacity.

Key features:

  • Dynamic growth: Lists automatically allocate more memory as needed, wherever the list is named from - a variable, a global, or a list parameter naming the caller's list
  • Mixed types: Appends of different types are allowed in any order; each element is printed by its own type, never by the list's (see Printing a List above)
  • Works with any value: integers — a negative literal included, append -5 to nums. — floats, strings, booleans, nothing, variables, function calls, arithmetic, and the collection reads element N of <list>, byte N of <buffer> and <name>'s <property>
  • to is the separator, not an operator. The value ends at the to that names the destination, so a value that would otherwise read to as a word of its own — a call written 'twice' to i — is written in braces: append {'twice' to i} to nums. Braces hand the enclosed tokens to the general expression parser, exactly as they do in a value slot elsewhere (append {i multiply i} to squares.).

Examples:

(Append integers)
a list called nums is [].
append 10 to nums.
append 20 to nums.

(Append strings)
a list called words is [].
append "hello" to words.
append "world" to words.

(Append from variables)
a number called x is 42.
append x to nums.

(Append in loops)
a list called squares is [].
a number called i is 1.
While i is less than or equal to 5,
  append i multiply i to squares,
  increment i.

Loop Expansion with Collections

The each...from syntax works with lists and ranges to execute an action for each item:

(Print each item from a list)
print each number from [1, 2, 3].

(Print each item from a range)
print each number from 1 to 10.

(Call a function for each item)
double of each n from [1, 2, 3].

(Append each item from a collection)
a list called source is [1, 2, 3].
a list called dest is [].
append each x from source to dest.

Syntax: <action> each <variable> from <collection>

Supported collections:

  • Lists: [1, 2, 3], any list variable
  • Ranges: 1 to 10, start to end (inclusive)
  • Arguments: arguments's all

Works with any action:

  • print each X from Y - print each item
  • function of each X from Y - call function for each item
  • append each X from Y to Z - append each item to a list
  • open ... at each X from Y - open file for each path

Examples:

(Print each from list)
print each n from [10, 20, 30].

(Print each from range)
print each n from 1 to 5.

(Function call with loop expansion)
To double of a number called x.
  Return a number, x multiply 2.

print double of each n from [1, 2, 3].

(Append from range)
a list called range_list is [].
append each n from 1 to 5 to range_list.

(Append from list)
a list called source is [10, 20, 30].
a list called dest is [].
append each x from source to dest.

(Empty collection - does nothing)
print each n from [].

Chained clauses: the grid

and joins any number of each clauses in one sentence. The action runs once per element of the Cartesian product, row-major (leftmost clause = outermost loop):

'pair' of each x from [1, 2] and each y from [10, 20].
(triple grid: a list and two ranges, 2 x 2 x 2 = 8 calls)
'triple' of each first from [1, 2] and each second from 1 to 2 and each third from 7 to 8.

A fixed argument may appear in any position among the clauses:

'pair' of 5 and each y from [10, 20].
'pair' of each x from [1, 2] and 99.

Arity is checked. The number of argument clauses must equal the callee's parameter count, just as for an ordinary call. A one-value action supplied two each clauses is a compile error, not a concatenation:

print each x from [1, 2] and each y from [3, 4].
(`print` takes one value but this sentence supplies more than one argument clause.)

This is what stops print each x from A and each y from B from being misread as printing both on one line. The single-value specialized forms (print, append, open) therefore take one clause only; a second each is the arity error above.

One asymmetry, kept deliberately: in print <func> of ... the grid form requires the first clause to be an eachprint pair of 5 and each y from B stays an error, because grid-parsing every printed call would change what print f of x add 1 has always meant (f(x) add 1). When a fixed argument must come first, use a plain call statement and print inside the function.

Empty collection anywhere → zero calls. If any clause's collection is empty, the whole grid produces no calls, regardless of position:

'pair' of each left from [] and each right from [10, 20].   (zero calls)
'triple' of each first from [1, 2] and each second from [] and each third from [5].  (zero calls)

Duplicate loop variables in one sentence are a compile error, naming the variable:

'pair' of each x from [1, 2] and each x from [3, 4].
(Loop variable 'x' is bound twice in one sentence.
 Each `each` clause must use a different name.)

but if attaches to the innermost iteration; its condition may reference every loop variable, since every loop is outside the conditional:

'pair' of each left from [1, 2, 3] and each right from [1, 2, 3], but if left is right print "diag".

After-loop values. Each loop variable retains its last-iteration value, independently — the same shadowing rule as a single clause, applied per variable. For a range clause, "last-iteration value" means what it means for a handwritten For each ... from 1 to N: the counter that ended the loop.

'pair' of each left from [1, 2, 3] and each right from [10, 20].
print the left.   (prints 3)
print the right.  (prints 20)

Zip is not the semantics. each x from A and each y from B is a Cartesian product, not a zip — matching comprehension syntax in Haskell, Python, and Rust. English's zip marker is respectively, which is reserved as a possible future marker for a zip mode; it is not parsed today.

Variable shadowing:

Loop variables shadow outer variables with the same name. After the loop, the variable retains the value from the last iteration:

a number called x is 100.
print the x.                  (prints 100)

print each x from [1, 2, 3].  (prints 1, 2, 3)

print the x.                  (prints 3 - last iteration value)

Conditional Branching with but if (Lists and Collections)

Use but if as a generic conditional branch over any base action, including inside loops and loop expansion:

(Print numbers, but override with words for certain values)
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".

(Conditional append in a loop)
append each number from 1 to 5 to out,
    but if the number modulo 2 is equal to 0 append 0.

How it works:

  1. The default action is the base statement.
  2. Each but if clause is checked in order.
  3. If a condition is true, that alternative action runs instead of the default.
  4. If no conditions match, the default action runs.
  5. An optional otherwise clause provides a final alternative.

Key points:

  • Conditions are checked in order - first match wins
  • Multiple but if clauses can be chained
  • The alternative action can be any valid Vox statement
  • otherwise provides a catch-all alternative
  • Works with both ranges and collections
  • The loop variable is available in conditions
  • In an append branch, the to <list/buffer> target may be omitted and is inherited from the base append statement; retargeting to a different list/buffer is not allowed

Inline Value Substitution with treating

The treating X as Y clause performs inline value substitution:

(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.

Input/Output

Print

Print "Hello, World!".
Print the x.
Print 'add numbers' of 3 and 5.

Print without newline:

Print "Loading: " without newline.
Print progress without newline.
Print "%".

Format Strings

Embed variables and expressions directly in strings using curly braces {}:

a text called name is "Alice".
a number called age is 25.
Print "Hello, {name}! You are {age} years old.".

Format Specifiers

SpecifierDescriptionExampleOutput
{var}Default formatting{name}Alice
{var:.N}N decimal places{pi:.2}3.14
{var:N}Pad to N characters{x:6} 42
{var:0N}Zero-pad to N chars{x:06}000042
{var:x}Hexadecimal (lowercase){n:x}0xff
{var:X}Hexadecimal (uppercase){n:X}0xFF
{var:b}Binary{n:b}101
{var:o}Octal{n:o}0o10
{var:04x}Padded hex{n:04x}0x00ff

The value inside {} must be a variable or expression, not a bare literal — {255:x} is rejected (255 is read as a variable name). The examples above assume a declared a number called n is 255. (set n to 5 or 8 for the binary and octal rows).

N is a count in both forms, and both render in full: {var:N} pads out to N characters and {var:.N} prints exactly N decimal places, correctly rounded (an exact tie goes to the even digit). Neither is capped — a very large N is simply a very large amount of output — but N has to be a count the compiler can hold, at most 9223372036854775807; past that it is a compile error naming the limit, not a width that quietly does nothing. A precision past the value's exact decimal expansion pads with zeros, since the expansion has ended and not because accuracy has run out: a float is a binary fraction, so it always has an exact finite expansion, and {pi:.50} prints all fifty places of it. A whole number has an exact expansion too — itself, then zeros — so {n:.2} on a number called n is 255. prints 255.00, and prints it exactly for every number Vox can hold.

A specifier has to be one the value's type can answer. A width asks nothing of a type: every rendering is some number of characters long, so {var:N} is accepted whatever var is (on a float or a text the value is rendered and the padding is not applied yet). The other two do ask something, and asking it of the wrong type is a compile error naming the way out, not a wrong answer:

  • {var:.N} counts places in a number's decimal expansion. A number, a float and a boolean have one; a text or a buffer does not.
  • {var:x}, {var:X}, {var:b} and {var:o} write a whole number in another base. A number and a boolean are whole numbers. A float is not — {ratio:b} is refused rather than quietly dropping the fraction, so write {ratio as a number:b} when that is what you meant — and neither is a text or a buffer.

Inside an expression hole the cast has nowhere to go — the braces a whole-expression cast needs are the hole's own — so work the value out into a number first and render that:

a float called ratio is 2.5.
a number called total is {ratio multiply 2.0} as a number.
Print "{total:x}".                (0x5)

A value, a list and a map render through their own routines, which ignore a specifier and print the value; a value's type is not known until it runs, so there is nothing to check.

The two compose: {var:8.2} asks for two decimal places, padded out to eight characters. The precision decides the digits and the width decides the padding, and each is honoured wherever there is something to honour it with — the same rule the width follows on its own. So {n:8.2} on a number called n is 255. prints 255.00, and {n:08.2} prints 00255.00; on a float the places are printed and the padding is not, because there is no float padder yet — exactly as a bare {f:8} prints the float unpadded. (A width composes with a radix too, which is the {var:04x} row above.)

Expressions in Format Strings

a number called x is 10.
a number called y is 3.
Print "Sum: {x add y}".
Print "Product: {x multiply y}".
Print "Arguments: {arguments's count}".

Format Strings as Values

Format strings are expressions, not just print arguments. Used as a value, a format string materializes into a fresh NUL-terminated string, so it works as a text initializer or assignment and survives being carried through lists (e.g. into an Execute argument list):

a buffer called word is 64 bytes in size.
copy hello to word.

a text called tok is "{word}".        (text from buffer contents)
a text called path is "/bin/{tok}".   (text from another text)

a list called cmdargs is [].
append tok to cmdargs.
Execute "/bin/echo" with arguments cmdargs.

Each evaluation allocates a new string; the source buffer can be cleared and reused without affecting texts already created from it.

Format Strings Everywhere

Every statement that takes a string value accepts a format string: write, buffer set/copy/append, filesystem paths (Create a directory called "{base}/{name}"), treating clauses, and function arguments. All sinks share one name resolver, so special names like {arguments's first} and {current time's hour}, format specifiers, and the 0x/0o hex/octal prefixes render identically whether the result is printed, written to a file, or built into a text or a buffer - a float's precision included: {ratio:.2} reads 2.50 in every one of them.

Declarations in Branches

A variable (or file handle) declared in EVERY branch of an if/otherwise chain definitely exists afterwards: it can be used after the branch and from inside functions, exactly like a top-level declaration. A name declared in only SOME branches remains scoped to its condition, and cross-condition use is still a compile error.

if 'output file' is empty then,
  Open a file for writing called output at 1.
Otherwise,
  Open a file for writing called output at 'output file'.

(output exists on every path - usable here and in functions)
write "hello\n" to output.

Escape Sequences

EscapeDescription
{{Literal {
}}Literal }
\nNewline
\tTab
\\Literal backslash

Example:

Print "Use {{braces}} for literal braces.".
Print "Tab:\there".
Print "Line1\nLine2".

Conditional Print

Print <default>, but if <condition> print <value>.

Chained conditions:

Print the number, but if <cond1> print "fizz buzz" but if <cond2> print "fizz" but if <cond3> print "buzz".

Rules:

  • First matching condition wins
  • Chain with but if or and if
  • Default value prints if no conditions match

File I/O

Buffers

Buffers are memory blocks for I/O operations. They come in two types:

Dynamic Buffers (default)

a buffer called inputbuf.
a buffer called data.

Features:

  • Start with zero capacity and grow automatically as needed
  • No buffer overflows possible - memory expands dynamically
  • Automatically freed on program exit

Fixed-Size Buffers

a buffer called small is 256 bytes in size.
a buffer called large is 8192 bytes in size.

Features:

  • Allocates exactly the specified capacity
  • Does NOT grow - a read or write past capacity is truncated at capacity and sets the error flag
  • Useful when you need predictable memory usage
  • User programs can check buffer length to detect truncation
  • Automatically freed on program exit

The size bound. A fixed buffer's size must be between 1 and 1073741824 bytes (1 GiB), and the bound holds however the size is written - a literal, a name, or a number the program only works out as it runs:

  • A size the compiler can see is refused where it is written, whether that is the literal a buffer called small is 0 bytes in size. or a name whose value is fixed for the whole program.
  • A size only run time can decide - one read from an argument, a file or a calculation - is refused when it is asked for. The buffer is made with no capacity and the error flag is raised, so On error catches it and the program carries on, exactly as it does for a fixed buffer that has become full.

A size of 0 is refused because a buffer with no fixed capacity is a dynamic buffer, which is declared with no size at all: a buffer called small.

Truncation Behavior: When reading into a fixed buffer that becomes full:

  • Reading stops and sets an error flag
  • Data beyond capacity is discarded
  • Program continues normally
  • Use On error to catch and handle the overflow

Object Properties

Access properties of objects using the 's syntax:

a number called len is mybuffer's size.
print myfile's size.

If mybuffer's size is equal to mybuffer's capacity then,
    print "Buffer is full!".

Universal Properties

Every variable has a type property that reports its declared type as text:

a number called n is 3.
a value called v is "hello".

print n's type.     (prints: Number (static))
print v's type.     (prints: Text (dynamic))
PropertyDescriptionExample
typeDeclared type name plus (static) or (dynamic)Number (static), Text (dynamic)

Statically-typed variables (number, float, text, boolean, list, map, buffer, file, time, timer) report their type with (static) because the compiler knows the type from the declaration. A value variable reports whatever its runtime tag currently holds, so it always uses (dynamic).

This property is intended for printing and logging. For type tests, use the is a <type> predicate — comparing the display string is stringly-typed and can drift from the predicate.

Buffer Properties

PropertyDescriptionType
sizeCurrent number of bytes storedNumber
lengthSame as sizeNumber
capacityMaximum bytes the buffer can holdNumber
emptyWhether the buffer has no data (size = 0)Boolean
fullWhether size equals capacity (for fixed buffers)Boolean

Example:

a buffer called data is 256 bytes in size.
Read from file into data.

If data's full then,
    print "Buffer is at capacity".

If data's empty then,
    print "No data was read".

Buffer Resizing

Resize a buffer to a new capacity:

a buffer called buf is 64 bytes in size.
resize buf to 256 bytes.
resize buf to 128.

Keywords: resize, reallocate, grow, shrink

Behavior:

  • Data is preserved up to min(old_length, new_capacity)
  • If shrinking below current data length, data is truncated
  • New buffer is allocated and old buffer is freed
  • Texts already made from the buffer with as text are independent copies, so resizing never disturbs them

Buffer Byte Access

Read and write individual bytes in buffers and strings by position. Positions are 1-indexed (like natural language: "the first byte", "the second byte").

Reading bytes:

a number called 'first' is byte 1 of data.
a number called 'byte value' is byte i of buf.

Writing bytes:

Set byte 1 of data to 0x48.
Set byte 2 of data to 'A'.
Set byte 3 of buf to value.

Creating buffer from string:

a buffer called buf is "Hello".
Set byte 1 of buf to 'J'.
Print buf.  (prints "Jello")

Modifying string bytes:

a buffer called msg is "Hello World".
Set byte 1 of msg to 'J'.
Print msg.  (prints "Jello")

Bounds Checking:

  • Out-of-bounds access sets an error flag and returns 0
  • Errors can be caught with On error
  • Buffer overflow is impossible - the compiler enforces bounds

What "in bounds" means differs for a write and a read, and the worked example below depends on it. A write (Set byte N of buf to ...) accepts any position from 1 up to the buffer's capacity: writing past the current size extends size to that position, zero-filling any gap (a dynamic buffer grows its capacity as needed). A read (byte N of buf) accepts positions from 1 up to the current size only - a byte that has never been written or appended is out of bounds even when the capacity has room for it. Position 0 is out of bounds for both.

Buffer Append and Copy

Efficiently combine buffers without byte-by-byte loops:

append source to destination.
copy source to destination.
clear destination.

set destination to "line {n:06}\t{content}".
a buffer called destination is "line {n:06}\t{content}".
append "line {n:06}\t{content}" to destination.
copy "line {n:06}\t{content}" to destination.

Behavior:

  • append source to destination adds source bytes to the end of destination.
  • copy source to destination replaces destination contents with source bytes.
  • clear destination sets destination length to 0 and preserves destination capacity.
  • When destination is a buffer, format-string sources are supported for set, is, append, and copy.
  • Format-string buffer writes are built in-place: literals/parts are appended directly to the destination buffer.
  • Dynamic destination buffers grow automatically as needed.
  • Fixed destination buffers truncate when full and set the error flag.
  • Source buffer is never modified.

Example:

Create a buffer called data with size 16.
Set byte 1 of data to 0xDE.
Set byte 2 of data to 0xAD.
Set byte 3 of data to 0xBE.
Set byte 4 of data to 0xEF.

a number called b1 is byte 1 of data.
Print "First byte: {b1:02X}".

(Out of bounds - caught by error handler)
a number called bad is byte 100 of data.
On error print "Index out of bounds!".

File Properties

PropertyDescriptionType
sizeFile size in bytesNumber
descriptorRaw file descriptor numberNumber
readableWhether file is open for readingBoolean
writableWhether file is open for writingBoolean
modifiedLast modification time (Unix timestamp)Number
accessedLast access time (Unix timestamp)Number
permissionsFile permission bits (e.g., 0644)Number

Example:

open a file for reading called src at "./data.txt".

print src's size.
print src's modified.

If src's size is greater than 1048576 then,
    print "File is larger than 1MB".

Checking whether a file exists. There is no exists property: every property above describes a handle that is already open, and a file that did not exist could not have been opened, so exists on a handle would be trivially true and answer nothing. The question worth asking — "can this path be opened?" — is answered by opening it and catching the failure with On error, the same pattern used for every other file operation that can fail:

open a file for reading called present at "./data.txt".
On error print "data.txt: cannot be opened".
If present's descriptor is greater than -1 then,
    print "data.txt: exists".

open a file for reading called missing at "./no-such-file.txt".
On error print "no-such-file.txt: cannot be opened".

data.txt: exists then no-such-file.txt: cannot be opened

A path-level exists predicate — asked before opening, with no handle involved — is a planned future addition; today the On error idiom above is how a program finds out.

List Properties (Object Properties)

PropertyDescriptionType
lengthNumber of items in the listNumber
sizeSame as lengthNumber
emptyWhether the list has no itemsBoolean
firstThe first item in the listItem
lastThe last item in the listItem

Example:

a list called names is ["Alice", "Bob", "Charlie"].

print names's length.

If names's empty then,
    print "No names in list".

List Element Access (Object Properties)

Access list elements by index. Indexes are 1-indexed (like natural language: "the first element", "the second element").

By index:

a list called nums is [10, 20, 30].

Print element 1 of nums.   (prints 10)
Print element 2 of nums.   (prints 20)

a number called i is 2.
Print element i of nums.   (prints 20)

By property:

Print nums's first.        (prints 10)
Print nums's last.         (prints 30)

Bounds Checking:

  • Out-of-bounds access sets an error flag. Where the compiler can prove the index is past the end, it returns the number 0 whatever the list's elements are, so read it into a number; where it cannot, it returns the destination's default value from the table under Two Canonical Forms0 for a number, the empty text for a text, [] for a list, {} for a map
  • Errors can be caught with On error

Example with error handling:

a list called items is [1, 2, 3].

a number called bad is element 100 of items.
On error print "Cannot access element 100 - out of bounds!".

Number Properties

PropertyDescriptionType
evenWhether the number is evenBoolean
oddWhether the number is oddBoolean
positiveWhether the number is > 0Boolean
negativeWhether the number is < 0Boolean
zeroWhether the number is 0Boolean
absoluteAbsolute valueNumber
sign-1, 0, or 1Number

Example:

a number called x is -42.

If x's negative then,
    print "x is negative".

print x's absolute.

Opening Files

Open files for reading, writing, or appending:

open a file for reading called source at "./data.txt".
open a file for writing called output at "./result.txt".
open a file for appending called log at "./log.txt".

You can also open an existing file descriptor directly by number:

open a file for reading called stdin_handle at 0.
open a file for writing called stdout_handle at 1.
open a file for writing called stderr_handle at 2.

When at is numeric, Vox treats it as a borrowed file descriptor instead of a filesystem path.

Flexible argument order: The clauses for reading/writing/appending, called <name>, and at <path> can appear in any order:

open a file at "./data.txt" for reading called source.
open a file called output for writing at "./result.txt".
open a file at "./log.txt" called log for appending.

Modes:

  • reading - Read from existing file
  • writing - Create/overwrite file
  • appending - Add to end of file

at value rules (compile-time validation):

  • Use text for filesystem paths: at "/path/to/file"
  • Use integers for file descriptors: at 0, at 1, at 2
  • File descriptor literals must be in range 0..2147483647
  • Invalid types (for example at 1.5 or at true) are compile-time errors

Reading

At a glance:

  • Use Read from ... into ... when you want to read raw bytes in chunks.
  • Use Read line from ... into ... when you want one logical line at a time.

High-level behavior:

  • Read replaces the buffer's contents with the bytes read; each Read continues from the file's current position, so it is best for bulk/stream processing.
  • Read line replaces the buffer with the next line and is best for line-by-line loops.
  • Both can read from files or standard input.

Read from files or standard input into a buffer:

Read from standard input into buf.
Read from source into contents.

Read one logical line (up to \n or EOF) into a buffer:

Read line from source into linebuf.
Read line from standard input into linebuf.

Read line behavior:

  • Includes the trailing newline in the buffer (when a newline is present)
  • Returns an empty buffer at EOF
  • Resets buffer contents before each read (replace, not append)
  • For fixed-size buffers, overlong lines are truncated and set the error flag

Seeking

Move a file descriptor position before reading:

Seek source to line 1.
Seek source to byte 1.
Seek source to bytes 128.

Seeking rules:

  • Positions are 1-indexed (line 1 = start of file, byte 1 = file offset 0)
  • Seek ... to line N moves to the first byte of line N
  • Seek ... to byte N/bytes N moves to byte position N
  • Invalid targets (e.g. line past EOF, position < 1, invalid fd) set the error flag, which On error catches
  • Line N exists when the file holds at least N-1 newlines before it, so a file that ends in a newline has one empty last line to seek to; anything beyond that is past EOF and sets the flag

Writing

Write strings, buffers, or special values to files:

Write "Hello, World!" to output.
Write buf to output.
Write a newline to output.

Write takes a text, a buffer, or a format string; a bare number, float, or boolean is a compile error, because a scalar holds a value where Write needs the address of some bytes. Render it with a format string instead:

a number called n is 72.
Write "{n}" to output.

A value is refused for the same reason — its type is only known at runtime, so the compiler cannot tell a text it could write from a number it could not. Copy it into a typed variable and write that:

a value called anything is "dynamic".
a text called settled is anything.
Write settled to output.

Writing rules:

  • A failed Write sets the error flag and is catchable with On error — a write the system refused (no space, a handle opened for reading, a closed or never-opened handle) or one that transferred fewer bytes than asked for:
Write buf to output.
On error print "Write failed!", exit 1.

Closing Files

Close file handles when done:

Close the source.
Close output.

File Operations

Check if a file (or any path) is available:

If "data.txt" is available then,
    print "File found.".

is available (compiles to access(2) with F_OK) is the correct, current form of this check. It works on any path expression - string literal, text variable, or buffer - and is not limited to plain files; see Directories, Mounting, and Process Control for how it is used to poll for a device node.

Negate with is not available:

While the root_device is not available,
    Sleep for 100 milliseconds.

Delete a file:

Delete the file "data.txt".

Error Handling

Operations that can fail (file reads, buffer operations, out-of-bounds access) set an error flag.

On Error Handler

Check for errors after specific operations with On error:

Read from source into buf.
On error print "Read failed or buffer overflow!".

Catchable Errors:

  • Out-of-bounds list/buffer access
  • Fixed buffer overflow (data exceeds capacity)
  • File operation failures — opening, seeking, reading, writing and deleting alike. A failed Write sets the flag, and so does a Read from, a Read line from or a Write on a handle whose own open failed.

Error Handling Patterns:

(Handle file read errors)
Read from file into buffer.
On error print "Read failed!", exit 1.

(Handle out-of-bounds access)
a number called item is element 100 of mylist.
On error print "Index out of bounds!".

(Check buffer state manually)
If buffer's size is equal to buffer's capacity then,
    print "Warning: buffer may have been truncated".

Resource Safety

vox provides memory safety through automatic resource management.

Memory Safety Guarantees

GuaranteeHow It's Enforced
No buffer overflowsBuffers grow dynamically as needed
No use-after-freeResources tracked and cleaned at exit
No resource leaksAutomatic cleanup of all FDs and buffers
No manual memory managementCompiler handles allocation/deallocation

Automatic Cleanup

All resources are automatically cleaned up on program exit:

a buffer called data.                    (Auto-freed on exit)
open a file for writing called log at "x". (Auto-closed on exit)
(Even if you forget to close - it's handled!)

Dynamic Buffers

Buffers start at zero capacity and grow automatically. No size specification needed:

a buffer called inputbuf.     (Grows as needed - never overflows)
Read from source into inputbuf. (Safe regardless of file size)

Internal structure:

  • 8 bytes: capacity (current allocation size)
  • 8 bytes: length (bytes used)
  • N bytes: data (grows via reallocation)

File Descriptor Tracking

Files are tracked at runtime for guaranteed cleanup:

  1. On open: FD registered in tracking table
  2. On close: FD unregistered from table
  3. On exit: All remaining FDs automatically closed

This works correctly even with conditional file operations:

If condition is true then,
    open a file for writing called log at "debug.log",
    Write "Debug info" to log.
    (Close might be forgotten here - still safe!)

Safety vs C Comparison

IssueC BehaviorVox Behavior
Buffer overflowUndefined behavior, security vulnerabilityImpossible - buffers auto-grow
Forgot to close fileResource leakAuto-closed on exit
Forgot to free memoryMemory leakAuto-freed on exit
Double freeUndefined behaviorTracked - can't happen
Use after freeUndefined behaviorNot possible by design

Directories, Mounting, and Process Control

These constructs were added for writing early-userspace/init-style programs in Vox - see examples/initramfs.vox for a complete, working early-userspace init sequence exercising all of them together.

Directories

Create a directory called "/proc".
Remove the directory called "/proc".
Delete the directory "/proc".
Change directory to "/newroot".

Rules:

  • Create a directory called '<path>'. - mkdir(2), mode 0755. The article (a) is optional; called is required.
  • Remove the directory called '<path>'. / Delete the directory "<path>". - rmdir(2). Both Remove and Delete work; the and called are optional.
  • Change directory to "<path>". - chdir(2).
  • All three set the error flag on failure - use On error to catch it.

Mounting Filesystems

Mount "proc" at "/proc" with type "proc".
Mount "tmpfs" at "/dev/shm" with type "tmpfs" with options "size=64m".
On error print "mount failed", exit 1.

Unmount "/dev/shm".
Unmount "/dev/shm" lazily.
On error print "unmount failed".

Rules:

  • Mount "<source>" at "<target>" with type "<fstype>" [with options "<options>"]. lowers directly to mount(2). source/target/fstype/options accept string literals, text variables, or buffers (including format-string-built buffers).

  • Moving/binding an already-mounted filesystem uses fstype "none" with options "move" or options "bind" - Vox recognizes this pattern and translates it into the correct MS_MOVE/MS_BIND mount flags:

    Mount "/proc" at "/newroot/proc" with type "none" with options "move".
  • Unmount "<target>". - umount2(2). umount is accepted as an alias for Unmount. Append lazily for MNT_DETACH (detaches immediately and releases the mount once nothing is using it any longer, instead of failing with "device busy") - needed when unmounting a filesystem your own running program was loaded from.

  • Both set the error flag on failure.

Device Nodes

Create a device node called "/dev/null" with type "c" major 1 minor 3.
Create a device node called "/dev/loop0" with type "b" major 7 minor 0.

mknod(2). type is "c" (character device) or "b" (block device); major/minor are the standard Linux device-driver identification numbers (see man 4 null/the kernel's Documentation/admin-guide/devices.txt for the registry of standard values). Sets the error flag on failure.

Create symbolic link from "/proc/self/fd" to "/dev/fd".

symlink(2): Create symbolic link from '<target>' to "<linkpath>". Sets the error flag on failure.

Switching the Root Filesystem

Pivot root to "/newroot" with old root "/newroot/oldroot".

pivot_root(2). put_old (the second path) must be a directory that already exists inside new_root - create it after mounting the new root, not before. After a successful pivot, the previous root filesystem is accessible at put_old's path relative to the new root (here, /oldroot), and should typically be released with Unmount "..." lazily once your program has chdir'd away from it. Sets the error flag on failure.

Executing Programs

Execute "/bin/sh".
Execute "/bin/echo" with arguments ["hello", "world"].

a list called cmdargs is ["hello", "world"].
Execute "/bin/echo" with arguments cmdargs.

On error print "execve failed", exit 1.

execve(2) - replaces the current process image entirely. Three forms:

  • No arguments: Execute "<path>". synthesizes argv = [path, NULL] (argc 1).
  • Literal argument list: Execute '<path>' with arguments [...]. - argv is built at compile time.
  • List variable: Execute '<path>' with arguments <list>. - argv is built at runtime from the list's current length and contents, sized and bounds-checked from that single length read so the argv array cannot be overrun regardless of the list's contents.

The environment is inherited from the calling process in all three forms. execve only ever returns on failure (there is no "success" path to return to - the process image is gone), so On error after Execute is the normal and only way to detect that it didn't work.

Process Control: fork and reap

Set pid to fork the process.
If pid is 0 then,
    (this branch runs in the child)
    Execute "/bin/some-program".
If pid is greater than 0 then,
    (this branch runs in the parent - pid holds the child's real PID)
    Set reaped to reap any child process.

These are expressions, not statements - use them anywhere an expression is valid (typically the right-hand side of Set/a number called ... is).

  • fork the process (the trailing the process is optional; bare fork also works) - fork(2). Returns 0 in the child, the child's PID in the parent, or a negative value on error. Sets the error flag on failure.
  • reap any child process - wait4(2) with pid = -1, waiting for any child. Returns the reaped child's PID, or a negative value on error.
  • reap process <pid-expr> / reap child <pid-expr> - wait4(2) for a specific PID.

Both set the error flag on failure (e.g. On error after reap process 999999 catches ECHILD when the PID is not actually your child).

Non-blocking reap: without waiting

Any reap form takes a without waiting suffix, which calls wait4(2) with WNOHANG instead of blocking:

Set r to reap any child process without waiting.
Set r to reap child pid without waiting.
Set r to reap process pid without waiting.

The return value is the whole point of the form, and the three cases must be told apart:

  • a child finished → its PID, error flag cleared;
  • children exist but none has finished → 0, error flag cleared (this is not an error — it is how you tell "still running" from "gone");
  • genuine error, e.g. no such child (ECHILD) → negative, error flag set, catchable with On error.

A non-blocking reap that returns 0 reaps nothing, so it does not disturb the reaped status (below) — only a reap that actually returns a child's PID changes it. without is already a reserved keyword (it is the print ... without newline token), so the suffix cannot be confused with a call argument after the pid expression, and waiting remains an ordinary identifier everywhere it is not this suffix.

The reaped status

Set r to reap child pid.
Set status to the reaped status.

the reaped status is an expression yielding the raw wait4 status word as a plain number — exactly the int status the kernel writes, undecoded. It reflects the most recent successful reap in the current process. Before any successful reap it is -1, a sentinel no real status can take, so "never reaped" is distinguishable from "exited 0". The sentinel lives in loader-initialized .data, not .bss, because _start (which would zero a .bss global) is only emitted for executables — a --shared library would otherwise read 0 and silently report "exited cleanly" with no child ever reaped.

reaped stays an ordinary identifier: the reaped status is consumed only as that exact phrase, and the reaped followed by anything else is an ordinary variable reference. (tests/102_fork_reap.vox does Set reaped to reap any child process. and keeps passing.)

Decoding the status

The compiler knows nothing about the wait-status encoding — the reaped status hands back the raw word, and a program decodes it with divide, modulo, and bit-and. Vox has no standard library on purpose, and this feature is complete with nothing installed:

To 'exit code of' with a number called status.
  Return a number, status divide 256 modulo 256.

To 'signal of' with a number called status.
  Return a number, status bit-and 127.

For ready-made decoding — these two plus crashed and 'exited normally', matching the <sys/wait.h> macros — the process library lives at Vox-lang/vox-libs, installable as an ordinary shared library:

see process version "0.1" from "./libprocess.lib".

It provides four functions over the raw status word, matching the <sys/wait.h> macros: 'exit code of' (bits 8–15), 'signal of' (the low 7 bits), crashed (true if a signal killed it), and 'exited normally' (true if no signal was involved). Use them at the call site, where they read as English:

If crashed of status then,
    Print "died by signal {'signal of' of status}".
If 'exited normally' of status then,
    Print "exit {'exit code of' of status}".

A supervisor loop, with no shelling out

These pieces compose into a complete supervisor — poll a child with non-blocking reap, time it out, kill it, and report how it died — using only Vox, no /bin/sh and no coreutils. examples/supervisor.vox is this loop as a runnable program, supervising both a job that finishes and a job that hangs:

see process version "0.1" from "./libprocess.lib".

Set pid to fork the process.
If pid is 0 then,
    Exit 0.

a timer called clock.
Start the clock.
a boolean called 'child is still running' is true.
a boolean called 'child was killed' is false.
While 'child is still running',
    Set 'reap result' to reap child pid without waiting,
    If 'reap result' is pid then,
        Set 'child is still running' to false.
    If 'child is still running' then,
        a number called 'milliseconds waited' is the clock's elapsed in milliseconds,
        If 'milliseconds waited' is greater than 5000 then,
            Send signal 9 to process pid,
            Set 'reap result' to reap child pid,
            Set 'child is still running' to false,
            Set 'child was killed' to true.
    If 'child is still running' then,
        Wait 10 milliseconds.

If 'child was killed' then,
    Print "hang".
If 'child was killed' is false then,
    Set status to the reaped status,
    If crashed of status then,
        Print "died by signal {'signal of' of status}".
    If 'exited normally' of status then,
        Print "exit {'exit code of' of status}".

A note on timing: the clock's elapsed in milliseconds reports true milliseconds, so the 5000-millisecond deadline above fires accurately at the five-second mark.

Send a signal: Send signal

Unlike fork/reap, this is a statement, not an expression:

Send signal <N-expr> to process <pid-expr>.

It performs kill(2) (syscall 62): <pid-expr> is the target PID (loaded into rdi), <N-expr> is the signal number (loaded into rsi). child is accepted as an alias for process, mirroring reap process/child:

Send signal 9 to child pid.

On success it clears the error flag; on failure (ESRCH no such process, EINVAL invalid signal, EPERM not permitted) it sets it, exactly like the other syscall statements, so On error catches the failure:

Send signal 0 to process 999999.
On error print "no such process".

Signal 0 is the standard existence check: it delivers nothing but returns an error if no process has that PID, which makes it a safe way to probe the error path. A common pattern is to send a real signal to a forked child and reap it:

Set pid to fork the process.
If pid is 0 then,
    Wait 30 seconds.
If pid is greater than 0 then,
    Send signal 9 to process pid.
    Set reaped to reap any child process.
    If reaped is pid then,
        Print "sigkilled child reaped with matching pid".

System Control: Shutdown, Reboot, Halt

Shutdown.
On error print "shutdown failed - are you root?".

Reboot.
Halt.

reboot(2), requiring CAP_SYS_BOOT (root). Each statement calls sync(2) first to flush filesystem buffers, then issues the matching command:

StatementAliasesCommand
ShutdownPoweroffLINUX_REBOOT_CMD_POWER_OFF
RebootRestartLINUX_REBOOT_CMD_RESTART
Halt-LINUX_REBOOT_CMD_HALT

On success, none of these return - the machine powers off/restarts/halts. On failure (not root, or no CAP_SYS_BOOT), the error flag is set instead of crashing or exiting, so On error safely catches the failure and execution continues - an unprivileged or accidental invocation can never bring down the machine.

Time and Timers

Getting Current Time

Get the current date/time as a time value:

Get current time into now.
a time called now is current time.

Time Properties

Access components of a time value using the 's property syntax:

PropertyDescriptionType
hourHour of day (0-23)Number
minuteMinute (0-59)Number
secondSecond (0-59)Number
dayDay of month (1-31)Number
monthMonth (1-12)Number
yearYear (e.g., 2026)Number
unixUnix timestamp (seconds since epoch)Number

Example:

Get current time into now.
Print "Current time: ".
Print the now's hour.
Print ":".
Print the now's minute.
Print ":".
Print the now's second.

Print "Date: ".
Print the now's year.
Print "-".
Print the now's month.
Print "-".
Print the now's day.

Inline Time Access

Access current time properties directly without storing:

Print "It is currently hour ".
Print current time's hour.
Print " of the day.".

Sleep / Wait

Pause program execution for a specified duration:

Wait 1 second.
Wait 2 seconds.
Wait 500 milliseconds.
Sleep for 3 seconds.

Syntax variations:

  • Wait <N> second. / Wait <N> seconds.
  • Wait <N> millisecond. / Wait <N> milliseconds.
  • Sleep for <N> seconds.
  • Sleep for <N> milliseconds.

Timers

Timers are stopwatches for measuring durations. They track start time, end time, and elapsed duration.

Creating a Timer

Create a timer called 'job timer'.
a timer called benchmark.

Starting and Stopping

Start the 'job timer'.
(... do work ...)
Stop the 'job timer'.

Alternative spellings:

  • Start / Begin
  • Stop / Finish

These four words are contextual, not reserved. They open a timer statement only when a name operand follows — Start the t., stop t. — and everywhere else they are ordinary identifiers, so a number called stop is 0. compiles, and a program may define and call its own zero-argument start. function. (End is not a Stop spelling: end belongs to the exit family of keywords and remains reserved.)

Timer Properties

PropertyDescriptionType
durationTotal duration (requires cast)Duration
elapsedElapsed time while running (requires cast)Duration
start timeWhen timer was started (unix timestamp)Number
end timeWhen timer was stopped (unix timestamp)Number
runningWhether timer is currently runningBoolean

Getting Duration

Use in to cast duration to a specific unit:

Print the 'job timer''s duration in seconds.
Print the 'job timer''s duration in milliseconds.
Print the 'job timer''s elapsed in seconds.

Complete Timer Example

(Measure job duration)
Print "Starting job...".
Create a timer called 'job timer'.
Start the 'job timer'.

(... do work ...)
Wait 1 second.
Print "Seconds elapsed so far: ".
Print the 'job timer''s elapsed in seconds.

Wait 500 milliseconds.
Stop the 'job timer'.

Print "Finished the job in: ".
Print the 'job timer''s duration in seconds.
Print " seconds".

(Access raw timestamps)
Print "Started at unix time: ".
Print the 'job timer''s start time.
Print "Stopped at unix time: ".
Print the 'job timer''s end time.

Formatted Time Output

Combine time properties with the zero-pad format specifier (see Format Specifiers) for formatted output. A time property can be read directly inside a format slot:

Get current time into now.
Print "{now's hour:02}:{now's minute:02}:{now's second:02}".
(Prints: 09:05:03)

Or, if you want the parts as named values first:

Get current time into now.
a text called h is "{now's hour:02}".
a text called m is "{now's minute:02}".
a text called s is "{now's second:02}".

Print "{h}:{m}:{s}".
(Prints: 09:05:03)

Command-Line Arguments

Access command-line arguments using the 's property syntax.

Arguments Properties

PropertySyntaxDescription
countarguments's countTotal number of arguments (including program name)
namearguments's nameProgram name (argv[0])
firstarguments's firstFirst user argument (argv[1])
secondarguments's secondSecond user argument (argv[2])
lastarguments's lastLast argument
emptyarguments's emptyTrue if no user arguments (argc ≤ 1)
allarguments's allUser arguments as a collection (for loop expansion)
rawarguments's rawOriginal unfiltered user arguments as a collection

Basic Usage

a number called argc is arguments's count.
Print "Argument count: ".
Print the argc.

a text called program is arguments's name.
Print "Program name: ".
Print the program.

Accessing User Arguments

(Get the first argument passed by the user)
If arguments's count is greater than 1 then,
    a text called username is arguments's first,
    Print "Hello, ",
    Print the username.
Otherwise,
    Print "Hello, World!".

Checking if Arguments Were Provided

If arguments's empty then,
    Print "No arguments provided.".

Dynamic Index Access

For accessing arguments by a computed index, use the argument at syntax:

a number called i is 2.
a text called val is the argument at the i.

Declarative Flag Parsing

Vox supports declarative CLI flag parsing with a schema-first style.

1) Declare a flag schema

Define each supported flag once, including aliases and type:

a flag called verbose is "-v" or "--verbose", it is a boolean.
a flag called output is "-o" or "--output", it is a text.
a flag called retries is "-r" or "--retries", it is a number.

Supported flag value types:

  • boolean (presence sets true)
  • text (consumes the next token as text)
  • number (consumes the next token and parses it as a number)

2) Optional schema modifiers

Flags may be marked as required and/or given defaults:

a flag called output is "-o" or "--output", it is a text with default "out.txt".
a flag called retries is "-r" or "--retries", it is a number and is required.
  • with default ... initializes the flag value if the flag is not passed.
  • and is required requires the flag to be present at runtime.
  • A flag with no with default that is not passed holds its type's empty value: "" for a text, 0 for a number, and false for a boolean. An unsupplied text flag is therefore safe to read, and can be tested with is empty.

3) Parse point: explicit or automatic

You can parse flags explicitly:

Parse flags.

Or omit it. If omitted, Vox inserts parsing automatically immediately after the last flag schema declaration.

4) Placement rules

Flag schema declarations are valid as long as they appear before parsing occurs.

  • You may place normal code before/between schema declarations.
  • You may use explicit Parse flags. to choose exactly when parsing happens.
  • Declaring new schemas after Parse flags. is a compile-time error.
  • Using flag variables before the parse point is a compile-time error.

5) arguments's all vs arguments's raw

After parsing:

  • arguments's all is the filtered positional argument view (recognized flags removed).
  • arguments's raw keeps the original user-provided argument sequence unchanged.

Example:

a flag called verbose is "-v" or "--verbose", it is a boolean.
a flag called output is "-o" or "--output", it is a text with default "out.txt".
Parse flags.

Print "output:{output}".

Print "ALL".
Print each item from arguments's all.

Print "RAW".
Print each item from arguments's raw.

6) Unix -- separator

-- stops flag processing. Tokens after -- are treated as positional arguments.

Example invocation:

myprog --verbose -- -v file.txt

In this case:

  • --verbose is parsed as a flag
  • -v after -- is treated as a normal positional argument

7) Practical pattern

a flag called help is "-h" or "--help", it is a boolean.
a flag called 'version' is "-V" or "--version", it is a boolean.
a flag called 'number' is "-n" or "--number", it is a boolean.

Parse flags.

If help then,
    Print "Usage: myprog [options] [files]".

If 'version' then,
    Print "myprog 1.0.0".

Print each item from arguments's all.

Environment Variables

Access environment variables using the 's property syntax.

Environment Properties

PropertySyntaxDescription
countenvironment's countTotal number of environment variables
firstenvironment's firstFirst env var (full "NAME=value" string)
lastenvironment's lastLast env var
emptyenvironment's emptyTrue if no environment variables
"NAME"environment's "HOME"Value of specific env var by name

Reading Environment Variables

a text called home is environment's "HOME".
a text called user is environment's "USER".
a text called shell is environment's "SHELL".

Print "Home: ".
Print the home.

Environment Variable Count

a number called 'env count' is environment's count.
Print "Total environment variables: ".
Print the env count.

Iterating Environment Variables

a text called env1 is environment's first.
Print "First env var: ".
Print the env1.

Checking if Variable Exists

If the environment variable "DEBUG" exists then,
    Print "Debug mode enabled".

Complete Example

(A greeter using the 's property syntax)

a text called name is "World".

(Use argument if provided, otherwise use environment variable)
If arguments's count is greater than 1 then,
    the name is arguments's first.
But if the environment variable "GREET_NAME" exists then,
    the name is environment's "GREET_NAME".

Print "Hello, ".
Print the name.
Print "!".

(Show some environment info)
a text called user is environment's "USER".
Print "Current user: ".
Print the user.

Note: The argument and environment variable functions are only included in the binary when used, keeping programs that don't need them small and efficient.

Operators

Arithmetic Operators

OperatorKeywords
Additionadd, plus
Subtractionsubtract, minus
Multiplicationmultiply, times
Divisiondivide
Modulomodulo, mod, remainder

Comparison Operators

ComparisonSyntax
Equalis equal to, is
Not Equalis not equal to, is not
Greater Thanis greater than
Less Thanis less than
Greater or Equalis greater than or equal to
Less or Equalis less than or equal to

Logical Operators (table)

OperatorKeyword
Andand
Oror
Notnot, isn't, aren't

isn't and aren't are contractions, and each stands for two words: isn't is is not, aren't is are not. Write them exactly where the spelled-out pair belongs — If v1 isn't v2 then, is If v1 is not v2 then, — and write a bare not everywhere no is/are belongs. These two are the only contractions in Vox: everywhere else an apostrophe is the possessive marker, a quoted name, or a character literal, and a word like don't or it's is not Vox (it's length is the possessive on a variable called it). See Names and strings.

Bitwise Operators

OperatorKeywords
Bitwise ANDbit-and
Bitwise ORbit-or
Bitwise XORbit-xor
Shift Leftbit-shift-left
Shift Rightbit-shift-right

Examples:

a number called lhs is 0b11110000.
a number called rhs is 0b10101010.

(Bitwise AND)
a number called result is lhs bit-and rhs.

(Bitwise OR)
Set result to lhs bit-or rhs.

(Bitwise XOR)
Set result to lhs bit-xor rhs.

(Bit shifting)
Set result to lhs bit-shift-left 2.
Set result to lhs bit-shift-right 4.

(Chained operations)
Set result to value bit-shift-right 8 bit-and 0xFF.

Keywords

Articles (Context-Dependent)

KeywordUsage
a, anDeclares new variable with type
theReferences existing variable

Statement Starters

KeywordPurpose
PrintOutput
Set, CreateVariable declaration
If, WhenConditional
WhileLoop
ForIteration
ToFunction definition
ReturnReturn value
IncrementAdd 1 to variable
DecrementSubtract 1 from variable
BreakExit loop
ContinueSkip to next iteration
ExitTerminate program with exit code
AppendAdd element to list
Create, Change, Remove/DeleteDirectories, device nodes, symlinks, chdir (see Directories, Mounting, and Process Control)
Mount, Unmount/UmountMount/unmount filesystems
Pivotpivot_root - switch the root filesystem
Executeexecve - replace the process image
Shutdown/Poweroff, Reboot/Restart, Haltreboot(2) - power off/restart/halt the machine
fork, reapProcess control expressions - fork(2)/wait4(2)
Send signalkill(2) - send a signal to a process (child aliases process)

Flag Schema

KeywordPurpose
FlagDeclare a command-line flag schema (a flag called ...)
ParseTrigger command-line flag parsing (Parse flags.)
RequiredMark a flag as required
DefaultSupply a default value for a flag

Connectors

KeywordPurpose
withFunction parameters, function arguments
called, namedVariable naming
of, to, onFunction arguments
andMultiple uses (see below)
orLogical OR
butConditional chaining
thenAfter condition
otherwise, elseAlternative branch
from, toRange bounds

The and Keyword

The word and has multiple context-dependent meanings:

ContextExampleMeaning
Logical operatorif x and y thenBoolean AND of two conditions
Function parameterswith a number called x and a number called ySeparates parameter declarations
Function arguments'add' of 3 and 5Separates argument values
Subject list terminatorx, y, and z are trueFinal item in comma-separated list before are

Disambiguation:

  • When and appears after a comma and before are, it's a list terminator
  • When and appears between two conditions (no comma), it's a logical operator
  • When and follows with/of/to/on, it separates arguments

Reserved Aliases

A few alternate spellings are also reserved because the compiler recognizes them as aliases for canonical keywords:

AliasCanonical keywordContext
msmillisecondsTime duration units (Wait 500 ms.)
messagetextType name (a message called ... is treated as text)
stringtextType name (already listed in the type synonyms)

These cannot be used as variable names. The diagnostic names the spelling you wrote and notes which canonical keyword it aliases — so a number called ms is ... reports 'ms' as an alternate spelling of 'milliseconds', not the internal canonical name.

Every keyword listed in the tables above is likewise reserved as a variable name. Two that are easy to hit by accident are worth calling out: the flag-schema keyword flag (a flag called ...) and the property keyword empty (x's empty). Writing a number called flag is 1. or a number called empty is 1. is rejected with the same "reserved keyword" diagnostic. (As with any reserved word, you can still quote the name — 'flag', 'empty' — if you genuinely need it.)

Two classes of special word

Not every word with a special meaning is reserved. Vox distinguishes:

  • Reserved keywords — banned as bare names everywhere, because they would be ambiguous anywhere: statement starters, operators (times, add), type names, connectors.
  • Contextual keywords — claimed only in the position where they mean something, and ordinary identifiers everywhere else: start/begin/ stop/finish for timers, send for signals, waiting in without waiting, available in is available, the things words, the property word name, and the property word count — claimed after a possessive marker and in the the argument count / the environment variable count phrases, so a number called count is 0. compiles while arguments's count keeps its meaning. The same treatment extends to the whole possessive/phrase family: capacity (also the with capacity N / of capacity N buffer phrase), raw, all (also the all the numbers from/between … range), first, last, second (also the Wait 1 second. unit — Set second to 1. Wait second seconds. compiles and waits one second), size and its synonym length (also with size N and N bytes in size), and version (the Library <name> version "…" and see <lib> version "…" headers). Each is a bare variable name everywhere except its one fixed grammatical position; arguments's first and a number called first is 0. both work in the same program.

The test for which class a word belongs in: if every position where the word means something is grammatically identifiable, it is contextual; only a word that would be ambiguous in ordinary positions is reserved.

Contextual Keywords (Things)

Three words the things feature claims only inside their construct, and treats as ordinary identifiers everywhere else — the same treatment send/begin/stop get for timers. None of them is a reserved variable name, so a number called thing is 1. and To do. (a function named do) both compile.

WordClaimed inElsewhere
thingA thing called <name> has ... (a definition)ordinary identifier
hasthe verb of a thing definitionordinary identifier
doTo do the <type>'s <member> (a member definition)ordinary identifier

A fourth, the, gains a second reading in this company: in To do the point's 'placed at' it pairs with a known identifier (the type), where a point's 'placed at' calls a maker that brings a new point into being. See the article rule.

Examples

Hello World

Print "Hello, World!".

Variables and Arithmetic

a number called x is 3.
a number called y is 5.
Print the x add the y.

Function Definition and Call

To 'add numbers' with a number called x and a number called y. Return a number, the x add y.

Print 'add numbers' of 3 and 5.

Counting Loop

Set the number called counter to 1.
While the counter is less than 10, print the counter, increment the counter.

FizzBuzz

To 'check divisibility' with a number called divisor and a number called dividend. Return a boolean, the divisor modulo the dividend is 0.

For each number from 1 to 15, print the number, but if 'check divisibility' of the number and 6 is true print "fizz buzz" but if 'check divisibility' of the number and 2 is true print "fizz" but if 'check divisibility' of the number and 3 is true print "buzz".

Libraries and Imports

The see Keyword

see pulls in code from another file. It has two distinct jobs:

  • see "<path>.vox". — include another Vox source file. This works today: the file is parsed as part of your program, so its functions become callable with no linking step. It is how you split a program across files.
  • see '<lib>' version "<ver>" from "<path>.lib". — consume a shared library through its .lib interface. This is the library path; see Shared libraries below.
see "./utils.vox".
see mathkit version "1.0" from "./libmathkit.lib".

There is exactly one library form. Three other shapes — see "./path.so"., see "lib" version "1.0" from "./path.so"., and see "./path.so" for "lib" version "1.0". — point see at a .so directly. A .so is binary ELF: it carries mangled symbol names but no Vox type information, so the compiler cannot check a call against it. All three are refused: see of a .so errors and directs you to the .lib, and the see ... for ... form has its own diagnostic — both name the canonical form see '<lib>' version "<x.y>" from "<path>.lib"..

Search paths. see resolves the path by its shape:

  • ./… or ../… — resolved against the directory of the file that contains the see statement, and only there.
  • /… — used as-is (absolute).
  • a bare name — /usr/share/vox/lib/<name> is checked first, and only if that does not exist does it fall back to the containing file's directory. Watch this: a bare see "utils.vox". can silently pick up a system file in preference to the one sitting next to your source. Write ./utils.vox when you mean the local one.

Those three shapes describe a .vox source include. A .lib path resolves differently: relative or bare, it is tried against the containing file's directory first and then each --lib-path directory, and its Location .so against the .lib's own directory first and then --lib-path; absolute paths are used as-is. --lib-path is not consulted for a .vox include at all; for --link it only passes search paths to the linker (-L). See Consuming a library.

Circular includes. The compiler tracks files already seen and skips a see that would re-enter one.

Shared libraries

A shared library is a .so you build from Vox and call from Vox — or from C, Rust, or any other host. The chain is:

.voxsee a .libLocation.so

The .lib is the typed interface (the .h equivalent); the .so it points at is linked, never read for types. This section covers writing one, the .lib file, consuming one, putting several libraries in one .so, and the symbol names a non-Vox caller needs.

What runs today. The whole path runs: building a library with --shared produces a self-contained .so plus its .lib interface, see of a .lib consumes it from Vox, export names are mangled, and multi-input --shared links several libraries (and several versions of one library) into one .so. A foreign host can also call the .so directly — see Calling a library from a non-Vox host.

Writing a library

Add a Library declaration at the top of a .vox file, then build with --shared:

Library mathkit version "1.0".

To 'add two numbers' with a number called x and a number called y. Return a number, x add y.

To greet.
  Print "hello from mathkit".
vox mathkit_lib.vox --shared -o libmathkit.so

That writes two files: libmathkit.so, and libmathkit.lib beside it — the typed interface a Vox consumer sees, described in The .lib file below. The .lib name comes from -o, so the pair always travels together.

This compiles to a self-contained shared object. It carries its own copy of the Vox runtime, so it is loadable from C, Rust, or any other host — not only from Vox. The runtime is position-independent, so a library may use the full core language — arithmetic, printing, buffers, files, floats, lists, maps — not a runtime-free subset. Only the library's own function definitions are exported; every runtime symbol is kept out of the dynamic symbol table.

Verify what you built:

$ nm -D --defined-only libmathkit.so
000000000000072c T mathkit_1_0_add_two_numbers
000000000000076c T mathkit_1_0_greet
$ readelf -r libmathkit.so
There are no relocations in this file.

Two exports and nothing else leaked; zero absolute relocations, so the whole object is position-independent. The labels are the mangled <library>_<version>_<func> form — mathkit_1_0_add_two_numbers and mathkit_1_0_greet — so two versions of one library can live in one .so without colliding; see Mangling below.

A library needs an identity. The Library declaration gives the library the name and version that drive mangling and the .lib. A --shared build with no Library line has no identity and is rejected — add the declaration.

Top-level statements are rejected. A shared library has no entry point, so a top-level executable statement (Print, assignment, If, a bare function call, …) would be silently dropped. The compiler rejects it instead:

error: Top-level print statement is not allowed in a shared library: only
function definitions, 'Library', and 'see' may appear at the top level.

Only function definitions, Library, and see may appear at the top level of a --shared compile. Put any work you need inside a function.

An empty library is rejected. A --shared compile with no function definitions exports nothing, which would yield a malformed version script and an opaque linker error. The compiler rejects it instead: a shared library must export at least one function.

The .lib file

The .lib is the public interface to a library: its name and version, where its .so is, and a table of contents of every exported function's signature. It is what a consumer sees, and the only place Vox types live — ELF carries mangled names but no types, so the .lib is the type source. A --shared build writes <output-stem>.lib beside the .so, one Library block per input. The .lib is a declared output like the .so, derived from the same -o: a rebuild overwrites it in place, so an edit-build loop needs no manual cleanup and anything hand-edited into a .lib is lost. The pair is written together, so a fresh .so never lands beside a stale .lib. The format:

Library mathkit version "1.0".
Location "./libmathkit.so".

Table of Contents:
    To 'add two numbers' with a number called x and a number called y, returning a number.
    To greet.
  • Library '<name>' version "<ver>". — the block's identity. Several Library blocks may appear in one .lib, each with its own Location; parsing runs to EOF, and a Library line starts a new block.
  • Location "<path>". — where the .so is. It resolves relative to the .lib first, then --lib-path, then error. Absolute paths are honoured but never generated, so a .lib is portable.
  • Table of Contents: — one line per exported function, in the same 11-type vocabulary as Vox source, in EITHER position: number, float, text, boolean, list, map, buffer, file, time, timer, value; anything else is an error naming the unsupported type. void isn't a spelling — a function returning nothing omits the , returning clause entirely — and neither is unknown, the compiler's own internal placeholder for an untyped parameter. A user-defined thing has no noun here either, so a --shared build refuses an export that takes or returns one — see .lib export of a thing is not yet supported.
  • A list may optionally carry its element type: a list of text called out, returning a list of text. This is compiler-inferred, not author- declared — Vox source has no generic/typed-collection syntax, so a library author still just writes a list called out; a --shared build scans the exported function's own body and, when every appended/returned element provably agrees on one type, writes list of <type> for you. Disagreement or no evidence emits plain untyped list, same as always. map's value type isn't carried this way; map stays element-untyped in both positions.
  • , returning a <type> exists only in .lib files. Vox source declares return types in the body (Return a number, x.), which a bodiless .lib declaration has no room for. No returning clause means the function returns nothing — which is also why a list's element type only shows up in the .lib when the exporting function has a declared return type (Return a list, out.) in the first place; a bare Return out. records no return type at all, list-element-typing included.

A .lib is lexed with the Vox lexer but parsed by a dedicated parser, so it cannot carry executable statements — only the interface above.

Consuming a library

see mathkit version "1.0" from "./libmathkit.lib".

a number called sum is 'add two numbers' of 3 and 4.
Print the sum.
$ vox mathkit_consumer.vox -o consumer
$ ./consumer
7

That is the whole consumer build — no --link, no -l, no -L. The see does the linking, because the .lib says where the .so is.

see of a .lib is the consumption path. The compiler:

  1. Resolves the .lib (relative to the source, then --lib-path).
  2. Parses it and selects the block matching name and version.
  3. Resolves Location relative to the .lib, then --lib-path.
  4. Verifies against the .so's dynamic symbol table — every mangled name the .lib promises must exist in the .so. This is the staleness check: a .lib that lies about a function is a compile error, not a runtime crash.
  5. Registers the signatures, so calls type-check like any other function.
  6. Emits extern <mangled> for each used function and adds the .so and an -rpath to the link line.

The -rpath in step 6 is the directory the .so was found in, recorded as an absolute RUNPATH. So the program finds its library where it stood at build time; move the .so afterwards and the loader will not find it, unless LD_LIBRARY_PATH points at the new directory.

Each failure is its own diagnostic naming the file and what was expected: missing .lib; no such library in it; version mismatch, listing the versions the .lib does offer; missing .so at Location; symbol absent from the .so (the stale-.lib case — it names the symbol); arity or type mismatch at the call site; and reading the result of an entry that has no , returning clause, which returns nothing, so there is no value to read — call it as a statement instead.

The stale-.lib case is the one you meet by hand-editing a .lib or by rebuilding a library with different exports:

Error: the .lib entry 'To 'ghostgreet' ...' promises the symbol
'mathkit_1_0_ghostgreet', but 'libmathkit.so' does not export it (not in
.dynsym).
The .lib is stale: it does not match the library binary. Rebuild the library
with `vox --shared` to regenerate the pair.

The worked example set in examples/ shows the workflow: mathkit_lib.vox is the library and mathkit_consumer.vox is the Vox consumer above. A foreign caller — C, Rust, or assembly linking the .so directly — is shown in Calling a library from a non-Vox host below.

Several libraries in one .so

vox a.vox b.vox --shared -o lib.so links several libraries into one .so in a single step — you cannot append to a linked .so, so one link step is the only way to combine them. The sources are parsed independently and then compiled into one unit, so the runtime is included once and shared by every library in the .so.

The reason this exists is backwards compatibility: two versions of the same library can live in one .so, kept apart by mangling. A consumer who upgrades the library keeps calling mathkit_1_0_add_two_numbers after mathkit_2_0_add_two_numbers ships beside it, with no recompile — both symbols are present and independently callable. That version isolation is why the whole design looks the way it does, and it is the case to keep in mind when the rest of it seems elaborate.

Duplicate <library, version> pairs across inputs are rejected with both filenames. Multi-input is --shared only; it is rejected for executable builds, where the semantics would be ambiguous.

Mangling

Every exported function is mangled to a single flat label:

<library>_<version>_<func>

mathkit + 1.0 + add two numbersmathkit_1_0_add_two_numbers. Each component is sanitized by mapping every character outside [A-Za-z0-9_] to _. The leading-digit prefix (a digit is not a legal C identifier start) applies only to the library component, which begins the symbol; the version and function components are interior and take the sanitizer alone, so the version 1.0 appears as 1_0 (the . becomes a single _, no prefix — applying the prefix per component would yield mathkit__1_0_add_two_numbers, a double underscore). A non-Vox caller — C, Rust, anything that links the .so — needs this mangled name to call the function at all, which is why the scheme is documented here and not only in docs/SYMBOL_MANGLING.md (the full rules, including what is and is not mangled). There is no unmangled alias: an alias would defeat the version isolation above.

Runtime state is not mangled — a deliberate non-goal. The runtime is emitted once per .so and shared by every library in it (one resource table, one .fini_array, one idempotent cleanup). Cross-.so isolation holds because each .so carries its own runtime and the version script hides it. Only function labels are mangled. See docs/SYMBOL_MANGLING.md.

Calling a library from a non-Vox host

A shared library is a plain .so, so any caller that can link one can use it — C, Rust, or hand-written assembly. This is also the case the mangling scheme above exists for: the foreign caller must name the export by its mangled label. Build the example library, then call it from a small assembly driver (nasm + ld only — the tools Vox already requires). Run these from the examples/ directory:

$ vox mathkit_lib.vox --shared -o libmathkit.so
$ nm -D --defined-only libmathkit.so
000000000000072c T mathkit_1_0_add_two_numbers
000000000000076c T mathkit_1_0_greet
; mathkit_driver.asm — link against libmathkit.so and call its exports.
global _start
extern mathkit_1_0_add_two_numbers
extern mathkit_1_0_greet

section .text
_start:
    and rsp, -16            ; 16-byte stack alignment for the Vox prologue
    mov rdi, 3
    mov rsi, 4
    call mathkit_1_0_add_two_numbers  ; mathkit_1_0_add_two_numbers(3, 4) -> 7, in rax
    cmp rax, 7
    jne .fail
    call mathkit_1_0_greet            ; prints "hello from mathkit"
    mov rax, 60             ; SYS_exit
    xor rdi, rdi
    syscall
.fail:
    mov rax, 60
    mov rdi, 2
    syscall
$ nasm -f elf64 -o mathkit_driver.o mathkit_driver.asm
$ ld -dynamic-linker /lib64/ld-linux-x86-64.so.2 -rpath '$ORIGIN' \
      -o mathkit_driver mathkit_driver.o -L. -lmathkit
$ ./mathkit_driver
hello from mathkit

The driver declares the exports extern and calls them with the Vox calling convention: integer arguments in rdi, rsi, … and the result in rax. -rpath '$ORIGIN' makes it find libmathkit.so in its own directory, so the pair is relocatable. (The .asm extension is gitignored under examples/ because the compiler emits .asm there as output, so this driver is shown here rather than tracked as a file — copy it out to run it.) The extern names are the mangled labels mathkit_1_0_add_two_numbers and mathkit_1_0_greet, matching what nm -D showed above.

Linking an executable against a .so directly

If the library has a .lib, you do not need this: see it, which registers its signatures and links its .so. --link is for a .so with no .lib — foreign, or hand-built — where the compiler knows no Vox signatures and only the linker is involved.

--link puts a built .so on the link line of an executable. It takes the library's soname stem — the part between lib and .so — so a file named libmath.so is linked as --link math:

$ vox hello.vox --link math --lib-path ./libs -o hello
$ readelf -d hello | grep -E 'NEEDED|RUNPATH'
 0x0000000000000001 (NEEDED)             Shared library: [libmath.so]
 0x000000000000001d (RUNPATH)            Library runpath: [./libs]

Because such an executable needs the dynamic loader at runtime, --link automatically adds the loader (/lib64/ld-linux-x86-64.so.2) and an -rpath for each --lib-path — but only when libraries are actually linked, so a plain vox hello.vox build stays a flat static binary with no loader dependency.

--link alone does not teach the compiler a library's function signatures, so it does not let Vox source call the library's functions — that is what see of a .lib does (it registers the signatures and adds the .so to the link line). --link is for the case where the program already references the symbols another way, or for a non-Vox driver assembled by hand — the Calling a library from a non-Vox host driver above is exactly that, linked with ld rather than --link.

Compiler Usage

Basic Usage (Compiler Invocation)

vox <source.vox> [options]

Options

OptionDescription
--emit-asmOutput assembly only (don't assemble/link)
--runCompile and run the program
--sharedBuild a shared library (.so) instead of executable
--link <libs>Link against shared libraries by soname stem (comma-separated). A Vox library with a .lib is linked by seeing it instead
--lib-path <paths>Additional library search paths (comma-separated)
-o <file>Output file name
-v, --verboseVerbose output

Examples

# Compile and run
vox hello.vox --run

# Build executable with custom name
vox hello.vox -o myprogram

# Build shared library
vox math.vox --shared -o libmath.so

# Consume a Vox library through its .lib (the `see` does the linking)
vox mathkit_consumer.vox -o consumer

# Link an executable against a .so that has no .lib (stem, not the lib prefix)
vox main.vox --link math --lib-path ./libs

Grammar Summary

program     ::= statement*
statement   ::= print_stmt | var_decl | assignment | if_stmt | while_stmt
              | for_stmt | func_def | thing_def | member_def | increment | decrement
              | break | continue | append_stmt

var_decl    ::= ("a" | "an") type "called" name "is" expr "."
              | ("Set" | "Create") "the"? type? "called"? name "to" expr "."

assignment  ::= "the" name "is" expr "."

append_stmt ::= "append" expr "to" name "."
              | "append" "each" name "from" expr ("treating" expr "as" expr)? "to" name "."

func_def    ::= "To" identifier (("with" | "of") params)? "." "Return" "a" type "," expr "."
params      ::= param ("and" param)*
param       ::= "a" type "called" name

func_call   ::= identifier ("of" | "with" | "to" | "on") args
args        ::= arg_clause ("and" arg_clause)*
arg_clause  ::= loop_expansion | expr

thing_def   ::= "A" "thing" "called" name "has" thing_entry ("," thing_entry)* "."
thing_entry ::= field_decl | member_decl
field_decl  ::= "a" type "called" name ("is" literal)?
member_decl ::= "a" "function" "called" name

member_def  ::= "To" "do" "the" name "'s" name (("," ("with" | "of"))? params)? "."
                body "Return" "a" name "," expr "."

if_stmt     ::= ("If" | "When") condition "then" "," block
                ("but if" condition "then" "," block)*
                ("otherwise" | "else")? ","? block? "."

while_stmt  ::= "While" condition "," block "."

for_stmt    ::= "For each" name "from" expr "to" expr "," block "."
              | "For each" name "in" expr "," block "."

print_stmt  ::= "Print" expr ("," "but if" condition "print" expr)* "."
              | "Print" "each" name "from" expr ("treating" expr "as" expr)?
                ("," "but if" condition "print" expr)* "."
              | "Print" identifier "of" "each" name "from" expr ("treating" expr "as" expr)?
                ("," "but if" condition "print" expr)* "."

loop_expansion ::= "each" name "from" expr ("treating" expr "as" expr)?

condition   ::= expr
expr        ::= or_expr
or_expr     ::= and_expr ("or" and_expr)*
and_expr    ::= not_expr ("and" not_expr)*
not_expr    ::= "not" not_expr | comparison
comparison  ::= additive (comp_op additive)?
additive    ::= multiplicative ((add | subtract) multiplicative)*
multiplicative ::= primary ((multiply | times | divide | modulo) primary)*
primary     ::= literal | identifier | func_call | "(" expr ")"

type        ::= "number" | "float" | "text" | "boolean" | "list"
              | "map" | "buffer" | "file" | "time" | "timer" | "value"
              | <user-defined thing name>   ; defined by `thing_def`
name        ::= identifier
identifier  ::= bare | quoted          ; see Naming Rules for the lexical rule
literal     ::= string | number | "true" | "false" | "nothing"
string      ::= '"' ... '"'            ; a string literal is data, never a name

Language reference