Reference

Variables

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

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.