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)
| Property | Description | Type |
|---|---|---|
length | Number of items in the list | Number |
size | Same as length | Number |
empty | Whether the list has no items | Boolean |
first | The first item in the list | Item |
last | The last item in the list | Item |
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 Forms —0for anumber, the empty text for atext,[]for alist,{}for amap - 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
listparameter 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 readselement N of <list>,byte N of <buffer>and<name>'s <property> tois the separator, not an operator. The value ends at thetothat names the destination, so a value that would otherwise readtoas 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 itemfunction of each X from Y- call function for each itemappend each X from Y to Z- append each item to a listopen ... 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 each — print 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:
- The default action is the base statement.
- Each
but ifclause is checked in order. - If a condition is true, that alternative action runs instead of the default.
- If no conditions match, the default action runs.
- An optional
otherwiseclause provides a final alternative.
Key points:
- Conditions are checked in order - first match wins
- Multiple
but ifclauses can be chained - The alternative action can be any valid Vox statement
otherwiseprovides a catch-all alternative- Works with both ranges and collections
- The loop variable is available in conditions
- In an
appendbranch, theto <list/buffer>target may be omitted and is inherited from the base append statement; retargeting to a different list/buffer is not allowed
Inline 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.