Voids For All!

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.

Contents

General information

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.

Types

void

void is the only primitive type in Voids For All; all other types are built out of voids. 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

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.

Maps

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.

Trees

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

Pointers

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

Functions

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.

for loops

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

Booleanish operators

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:

More list and map operations

More about list and map displays

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:

All of these work with ^: for trees.

More about subscripts

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:

Insertion and deletion

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.

Input, output, and strings

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[][]);
Displays a string.
void input(void)[][];
Inputs a line of text and returns it, without a trailing newline.
void format(void number[])[][];
Takes a number and returns a string representing that number in decimal notation. (E.g., format(123) is "123".)
void parse(void (*string)[][])[void][];
Takes a pointer to a string representing a number and parses it. Whitespace at the beginning of the number is ignored. If the number parsed successfully, returns {: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).

Interpreter

Input: