void
is a weird type, so I made a programming language about it. You cannot stare into the void, because that would be a type error.
A Voids For All program is a sequence of statements. Most of these statements end in a semicolon or a group of statements surrounded by curly braces (like in C and related languages). Like in C, anytime a statement is expected, a groups surrounded by braces can appear instead; this gives a new scope.
Voids For All is statically typed. Every variable must be declared and given an explicit type. If a variable is declared and not explicitly given a value, then it will be initialized with the default value of its type (generally an empty whatever-it-is). Variables can be assigned new values after they're initialized using the assignment operator, =
, which works just like in C.
Variable names can contain letters, digits, _
, and any non-ASCII characters, and cannot start with a digit. The words delete
, else
, for
, insert
, return
, and void
are reserved and can't be used as variable names; also, the names print
, input
, format
, and parse
are predefined, so you can't define new variables with those names.
The character @
causes any characters after it on the same line to spiral away into the void (i.e., it starts a comment). Otherwise, whitespace and newlines are ignored (outside string and character literals) except to separate tokens.
void
void
is the only primitive type in Voids For All; all other types are built out of void
s. It has only one value, making it not very useful by itself. In fact, it's so useless that you can't even make a variable of type void
, since there would be no point. If you could declare such a variable, it would be like this:
void x;
Expressions of type void
can only be used as statements. For instance, if you have a function foo
which returns void
, you can call it like this:
foo();
but not like this:
return foo();
even if you're inside another function that returns void
.
Lists are sequences of values of a given type. The length of a list is determined at runtime, and elements can be added or removed from the list. A list can be declared like this:
void x[]; @ list of void
If you want a list of lists, you can add more pairs of brackets:
void x[][]; @ list of lists of void void x[][][]; @ list of list of list of void void x[][][][]; @ list of list of list of list of void @ etc.
You can make a list by putting values in braces, separated by commas; leave a space blank to put a void into a list:
void x[] = {}; @ empty list void x[][] = {{}, {}, {}}; @ list of three empty lists void x[] = {,,,}; @ list of 3 voids
That last example was three voids, not four, because a trailing comma is allowed. Also, if it were four voids, then there wouldn't be a way to distinguish between 0 voids and 1 void.
You can get an element from a list by using square brackets, like, x[y]
. Since there's no primitive number type, y
is instead expected to be a list; its length is used as the index. So x[{}]
(0-length list) gives the first element, x[{,}}
or x[{{}}]
(1-length lists) give the second element, and so on. You can also assign to an element like x[{}] = value;
. The element must already exist to assign to it; insertion and deletion are explained later. (Note that void x[] = value;
assigns the whole list, whereas x[{}] = value
only assigns an element, just like in C.)
You can assign a list to another list (x = y;
or void x[] = y;
), and it will make a copy. Same is true for all data structures in Voids For All.
void
(i.e., void []
) are only distinguished by having different lengths. That means that a list of void
represents a number. As a shortcut, you can get a list of void
s simply by typing the number of voids you want (so 3
is the same as {,,,}
).
Maps are collections of key/value pairs, where each key exists at most once in a map. A map can be declared like this:
void x[void]; @ a map with void keys and void values void x[void[]]; @ a map with (list of void) keys and void values @ i.e., a map with number keys void x[void[void]]; @ a map with (a map with void keys and void values) @ keys and void values
Any type can be used as a map key.
For declaring more complex value types, Voids For All works like C, in that void x[void][]
means that x[(void value)][(list index)]
is of type void
(i.e., x
is a map of lists of void
).
Making maps is similar to making a list, except you put a colon between the key and the value. If either the key or the value is void, it can be left out, but you still need the colon:
void x[void] = {}; @ empty map void x[void] = {:}; @ map where void maps to void void x[void][] = {:{}}; @ map where void maps to the empty list void x[void[]] = {{}:}; @ map where the empty list maps to void
Just like for lists, you can access or change a value from a map by putting its key in brackets (leaving an empty pair of brackets for a void
key). Unlike for lists, you can add new elements just by assigning to them.
void
values (e.g., void x[void[]] = {1:, 2:, 3:}
) are only distinguished by which keys are in the map. This means that they represent sets.
void
keys (e.g., void x[void][] = {:1}
) can only have one value, but might have none. This means that they represent optional/nullable/Maybe types.
void
keys and void
values (i.e., void x[void] = {}
or {:}
). This means that they represent booleans. {}
is false, and {:}
is true.
There are two kinds of trees in Voids For All: list-trees and map-trees. A list tree is a list of lists of lists of ... etc., an arbitrary number of levels deep, with leaf nodes represented by the empty list. They're declared and created like this:
void ^x[] = {{}, {{}, {}}, {}};
Map-trees, likewise, are maps of maps of maps of ... etc., with all maps having the same type of key. They're declared and created like this:
void ^x[void[]] = {0:{}, 1:{2:{}, 3:{}}, 3:{}}
The branches of a tree can be accessed and modified in the same was as with lists and maps. Additionally, each list or map in a tree has a label, which can be accessed or changed with the ^
prefix operator. The examples above have void
labels, so they're effectively unlabelled trees. To declare a tree labelled with lists or maps, you'll have to put the ^x[]
in parentheses, since []
has a higher precedence than ^
:
void (^x[])[void]; @ list-tree labelled with void[void] @ (i.e., labelled with boolean) void ^x[][void]; @ list of unlabelled map-trees void ^(x[])[void]; @ same as above
To create a labelled tree, each pair of braces needs ^: label
as its first item:
void (^x[])[] = {^:1, {^:2}, {^:3, {^:4}}}; ^x; @ 1 ^x[0]; @ 2 ^x[1]; @ 3 ^x[1][0]; @ 4
void
is just an unlabe— oh, I already said that.
A pointer points to a value. If the same pointer is assigned to multiple variables, then changes to the value pointed to from one variable will affect the value read from any other variable. You can declare a pointer like this:
void *x; @ pointer to void void (*y)[]; @ pointer to number
And access it like this:
void z[] = *y; @ read the value pointed *y = z; @ write to the pointer
Unlike in C, you cannot make a pointer to an existing variable (there's no equivalent of &
), and you also don't have to explicitly allocate or free memory. Rather, declaring a pointer variable without initializing it will allocate a new object each time the declaration runs, and will initialize the new object to the type's default value. (This also means that pointers can never be invalid or null.)
void
(i.e., void *x;
) can't be read from, but it can be compared with other pointers. That means they're similar to JavaScript's symbols. (Note that this is different than C's void*
.)
A function is a thing that you can call, and it does stuff and returns a value. Functions can be defined like this:
void foo(void x[]) { @ code goes here }
and called like this:
foo(10);
If a function returns something other than void
, then it needs to end with a return statement, which has the form return expression;
. Return statements without an expression can be used in functions returning void
, and also at the top level (where they'll exit the program).
If a function is used before it's defined, then it must be declared first, like so:
void bar(void x[][])[]; @ ...other function definitions... void bar(void x[][])[] { return x[0]; }
In this case, the declaration creates a new function variable with its default value, which is a function that does nothing and returns the default value of its return type; then the function definition assigns to that variable. This means that if you call bar
before the definition runs at runtime, then it'll just do nothing, which is probably not what you want.
Functions are first-class. When you declare or define a function, that just defines a variable with a function type, and you can reassign it like any other variable, and you can do things like declare lists of functions (void foo[](void);
). (You do not need to declare it as a pointer like you do in C.) Also, if you define a function within another function, then that function is a closure (capturing all variables by reference). So you can do things like:
void makePairMaker(void x[])(void[])[][] { void makePair(void y[])[][] { return {x, y}; } return makePair; } makePairMaker(3)(4); @ {3, 4} void f(void[]) = makePairMaker(2); f(1); @ {2, 1}
When comparing two functions for equality (e.g., if you use a function as a key in a map), the functions are compared by reference.
void
don't return a value, and are presumably called for their side effects, just like in most other languages that have void
.
void
as a parameter don't take any parameters (like in C). These functions must be declared like void foo(void)
, not void foo()
; the latter is a syntax error. void
parameters are only allowed if the parameter list is just (void)
; things like foo(void x)
and foo(void, void)
are not allowed.
for
loopsThe only control structure in Voids For All (aside from function calls/return) is the for
loop. The general syntax of a for
loop is:
for [subscript_var] value_var = expression { statements } else statement
expression can be any expression that returns an array or a map. The statements will run for each element in the array or map. If the array or map specified is modified during the loop, the loop continues using the original elements. The expression can also be omitted, in which case it's an infinite loop.
subscript_var and value_var, if specified, must be new variables that aren't yet in scope. The types of these variables will be inferred (and can't be explicitly specified), and these variables cannot be assigned to. [subscript_var]
can be omitted (in which case the brackets must also be omitted), and value_var can be omitted (in which case the equals sign can optionally be omitted).
If expression is a list, then subscript_var will be assigned the index of the list (0, 1, 2, etc.), and value_var will be assigned the values. If expression is a map, then subscript_var will be assigned the keys of the map (in some unspecified order), and value_var will be assigned the corresponding values. If expression is omitted, then subscript_var will be assigned increasing integers (0, 1, 2, etc.), and value_var must be omitted. If either variable is omitted, then whatever value it would have been assigned is ignored; if either variable would have type void
, then it must be omitted.
If an else
clause is specified, then it will run if the loop would execute zero times, i.e., if the value of expression is empty. This can also be omitted (in which case the else
keyword must also be omitted). The main body of the for
loop must be surrounded by braces, but the else
clause doesn't need braces if it's a single statement.
There is no break
statement; the only way to exit a loop early (or to exit an infinite loop) is to return
from the current function.
A for
loop with an expression that can either have one or zero elements acts like an if statement in other languages. There are a few operators that are useful in this context:
expr[expr]?
returns {:expr[expr]}
if the corresponding element exists in the list or map, and otherwise {}
.
expr == expr
returns {:expr}
if the values of the two expressions are the same, and {}
otherwise. It can be chained, so expr == expr == expr == expr
returns {:expr}
if all expressions are equal, and {}
otherwise. (e1 == e2
is equivalent to {e1:e1}[e2]?
, except it evaluates e1
once.)
!expr
returns {:}
if expr is empty, and {}
otherwise. It's the only unary operator with a lower precedence than ==
. (!e
has the same number of elements as e == {}
.)
?expr
returns {}
if expr is empty, and a map with a single randomly-chosen key-value pair from the list or map otherwise. This means that
for ?l { statements; }
executes statements; exactly once if l
is non-empty, and zero times otherwise; but also for [k] v = ?map ...
chooses a random key/value pair from map
, for v = ?list ...
chooses a random value from a list, and for [n] = ?number ...
chooses a random number below number
.
I showed earlier that you can write a list like {x, y, z}
, and a map like {a:x, b:y, c:z}
. However, there are a couple other things you can do with that notation:
+expr
instead of one of the elements to insert all values of expr. In a list display, it concatenates the elements, so {x, +list, y}
returns list
with x
prepended and y
appended, and {+list1, +list2}
concatenates two lists. In a map display, it replaces any values already specified with the values from the new map, so {+map, key:newValue}
returns map
, but with key
replaced by newValue
, and {key:defaultValue, +map}
returns map
, but with key
associated with defaultValue
if it's not already associated with something.
{for [subscript_var] value_var = expression, clause}
. clause can be a single list element, a single key: value
pair, a single +expr
, or another for
clause (nested arbitrarily deep). clause can also be omitted for a list of void (i.e., a number; this means {for x}
returns the size of x
). for
clauses cannot be combined with other elements (so {x, for y, z}
and {for x, y, z}
are both errors; use +{...}
to clarify your meaning).
All of these work with ^:
for trees.
I mentioned earlier that you can put a ?
after a subscript to give an optional result. (Otherwise, accessing an out-of-bounds index or nonexistent key gives an error.) But also:
x[-1]
gives the last element, x[-2]
gives the second-to-last, etc. Note that there are no actual negative numbers in Voids For All; the minus sign is part of the syntax of the subscript. This can also be combined with the question mark.
x[-0]
gives one past the last element. This is always an error when accessed normally, but it's useful in slices and insertions (described later).
x[::2]
gives every other element, starting at the first; can be left out for a default of 1). Negation works with slices just like with regular subscripts; a negative step gives the elements in reverse order.
If the slice is out-of-bounds, then only parts of the slice that are in-bounds are returned. This means that a question mark isn't needed (and can't be used) with a slice.
Slices can be assigned to as long as they don't have a step.
You can insert into or delete from a list or map by using the insert
and delete
operators. Both of these require their operand to be a subscript or slice operation. For lists, these move the elements after the ones inserted or deleted to make room/not leave holes; for maps, these do not affect other elements.
insert list[subscript];
inserts an element at subscript (before the element currently at the subscript for lists, or replacing the existing value for maps), which has the default value for the element's type. To insert a specific value, use insert list[subscript] = value;
. (For maps, insert map[subscript] = value;
is equivalent to map[subscript] = value;
.) A subscript of -0
is useful here to indicate the end of the list. For lists, the subscript must be between 0 and the length of the list, inclusive, otherwise it's an error.
You can insert multiple values at a time with slices: insert list[start : end];
inserts enough values such that list[start : end]
is filled with the new values; insert list[start : ] = other_list;
inserts all the values from other_list at start (the end of the slice is ignored in this case).
delete list[subscript];
deletes the element at subscript and returns it. It's an error if the subscript doesn't exist; adding a question mark after the subscript makes it not an error. delete list[start : end];
deletes all the elements in the slice.
Modifying an array inside an expression that subscripts that same array (e.g., insert x[delete x[0]]
) is undefined behavior.
A string is represented by a list of numbers (void[][]
), each representing a Unicode character. Strings can be typed using single or double quotes, and some standard escapes are supported. You can also get a character code as a number by putting the character (or escape) after #
, so e.g. #a
is equivalent to 97
(or 'a'[0]
), and #\n
is equivalent to 10
.
There are four predefined functions for working with strings:
void print(void message[][]);
void input(void)[][];
void format(void number[])[][];
format(123)
is "123"
.)
void parse(void (*string)[][])[void][];
{:number}
and updates the value pointed to by string
to start at the first character after the number. If the string does not represent a number, returns {}
and doesn't change the pointer.
Also, there is one output-related operator: `
(backquote). `x
prints the value of x
(which can be any type) in an implementation-dependent format, and then returns x
. It also shows the type of x
at compile time. It can also be used as an lvalue or as part of a declaration. When used in a declaration, it only shows the type at compile time (does nothing at runtime). Also in declarations, it's helpful to put it in parentheses close next to the actual variable name; e.g., void `x[void][][]
shows the type of x[][subscript][subscript]
(which is just void
), whereas void (`x)[void][][]
shows the actual type of x
(which is an optional list of number).
for-header | → | for ([ name ] )? (name? = )? expr? | for loop |
clause | → | expr? | List item |
| | expr? : expr? | Map item | |
| | + expr | Insert all | |
expr | → | ( expr ) | Grouping |
| | name | Variable | |
| | number | Number (list of void) | |
| | " string " | ' string ' | String (list of number) | |
| | # character | Character literal (number) | |
| | { (^ : expr , )? (clause , )* } [1] | List/map display (see also lists, maps, trees) | |
| | { (^ : expr , )? (for-header , )* clause } [1] | for-loop list/map display | |
| | expr ( (expr separated by , )* ) | Function call | |
| | expr [ (- ? expr)? ] ? ? | Subscript | |
| | expr [ (- ? expr)? : (- ? expr)? (: - ? expr?)? ] | Slice | |
| | (* | ^ | ` | ? | delete | insert | ! ) expr | See below | |
| | expr (== expr)+ | Equality comparison | |
| | expr = expr | Assignment | |
decl | → | ( decl ) | Grouping |
| | name? | ||
| | decl [ ] | List | |
| | decl [ void decl ] | Map | |
| | decl ( (void decl separated by , )+ ) | Function | |
| | (* | ^ | ` ) decl | Pointer, Tree, (show the variable type) | |
statement | → | { statement* } | Block |
| | expr? ; | Expression statement | |
| | void (decl (= expr)? separated by , )+ ; | Variable declaration | |
| | void decl { statement* } | Function definition | |
| | return expr? ; | return from function | |
| | for-header { statement* } (else statement)? | for loop | |
program | → | statement* |
[1] A comma that is right before the closing }
of the display can be omitted
In order of precedence:
Associativity | Operator | Type | Description |
---|---|---|---|
Postfix | [expr] | T[] × U[] → T ; T[U] × U → T lvalue × rvalue → lvalue; rvalue × rvalue → rvalue | Subscript (see also lists and maps) |
[expr]? | T[] × U[] → T[void] ; T[U] × U → T[void] lvalue × rvalue → lvalue; rvalue × rvalue → rvalue | Optional subscript | |
[expr:expr] | T[] × U[] × V[] → T[] ; T^[] × U[] × V[] → T^[] lvalue × rvalue × rvalue → lvalue; rvalue × rvalue × rvalue → rvalue | Slicing | |
[expr:expr:expr] | T[] × U[] × V[] × W[] → T[] ; T^[] × U[] × V[] × W[] → T^[] rvalue × rvalue × rvalue × rvalue → rvalue | Slicing | |
(expr...) | T(U...) × U... → T rvalue × rvalue... → rvalue | Function call | |
Prefix | * | T* → T rvalue → lvalue | Pointer dereference |
^ | T^[] → T ; T^[U] → T lvalue → lvalue; rvalue → rvalue | Tree label | |
? | T[] → T[void[]] ; T[U] → T[U] rvalue → rvalue | One random element | |
insert | T → T (or T^[] → T^[][] for slices)[2]lvalue subscript/slice → lvalue | Insert element | |
delete | T → T lvalue subscript/slice → rvalue | Delete element | |
` | T → T lvalue → lvalue; rvalue → rvalue | Print type and value | |
Chaining | == | T × T × … → T[void] rvalue × rvalue × … → rvalue | Equality test |
Prefix | ! | T[] or T[U] → void[void] rvalue → rvalue | Negation/test if empty |
RTL | = | T × T → T lvalue × rvalue → rvalue | Assignment |
[2] This allows inserting a list that isn't a tree into a tree. The Undo interpreter example takes advantage of this on the line insert expr[-0:] = delete extraArgs[:];
(expr
is a tree, extraArgs
is not).
Input: