Winner of the -03
, "30 minutes or it's free", and "self-documenting" awards from Quirky Languages Done Quick!
Note: This language is a prototype built for the QLDQ mini-hackathon. It has some rough edges, and there are no promises or expectations that these will ever be polished away. Feel free to fork if you want to modify or improve (within MIT license allowances), and have fun! We'd be glad to hear about anything cool you come up with :)
A programming language built on the oft-overshadowed datatype, the queue. While languages like Forth and its ilk are based on stacks, QQ strives to take the road less travelled. QQ is a language whose power is that every advanced data structure, including the frame itself, is a form of queue.
And it's clearly very intuitive to use.
Here are some of the programs that language designers are contractually obligated to write:
hello_world.qq
"hello world"
print
fizz_buzz.qq
"fizzbuzz"
[
dup
100
rot
>
[ ret ]
rot
if
dup
3
rot
%
[
dup
5 rot
%
[ print ]
[ "buzz" rot print pop ]
rot
ifelse
]
[
"fizz" rot write pop
dup
5 rot
%
[ " " rot print pop ]
[ "buzz" rot print pop ]
rot
ifelse
]
rot
ifelse
1
+
1
rot
pack
"fizzbuzz"
rot
call
]
def
"fizzbuzz"
[ 1 ]
call
factorial.qq
"factorial"
[
dup
1
rot
!=
[
dup
[ ]
dec
rot
"factorial"
qpush
rot
call
rot
exec
*
]
rot
if
]
def
"loop_factorial"
[
[
1
dup
rot
==
[ break ]
dec
rot
if
dup
*
]
dup
loop
pop
]
def
"factorial"
[ 10 ]
call
exec
print
pop
"loop_factorial"
[ 10 ]
call
exec
print
There are in essence two places to store data. Unlike many "modern" programming languages, QQ eschews the use of random access variables. The only nameable things by default are functions, but everything else lives in a queue.
The main queue, which can become arbitrarily large, is called the qframe. It is the default space to store things and so serves as memory. qframes are scoped so that each function call takes place with its own qframe.
There is a secondary queue for convenience known as the register queue, and it can be allocated within a function using the rqalloc
command. When a register queue is allocated, a size must be specified, and the register queue that is created cannot hold more elements than its size. This makes it useful primarily as an alternative place to store data that is used repeatedly, but otherwise behaves like a normal queue.
Additionally, one can create additional Queues, but they are not independent and must be stored inside of either the qframe or the register queue. More on that in the datatypes section.
There are 4 different types of objects that can be put into memory: Integers, Strings, Booleans, and Queues.
Integers can be any whole number and behave like arbitrary-precision integers. Think of them like you do in Python or most other modern languages for the sake of basic arithmetic.
Strings can be of arbitrary length and have basic functionality for printing and concatenation (using +
).
Booleans can be true
or false
and are primarily used in conditions.
Queues are special, in that they can contain both data and operations. This is because a Queue can be used both for storing data and for storing instructions, like a code block.
QQ supports many basic binary math operations for its Integers, including + - * / % ** & | ^ == != < <= > >=
(where ^
is bitwise xor and **
is exponentiation).
Each of these operations pops the two elements off the front of the queue, performs the operation between them (using the frontmost element on the left side), and enqueues the result of the calculation. The comparison operations return Booleans.
QQ also supports some unary operations for basic datatypes: not
, inc
, and dec
(for increment and decrement respectively). They pop off only one element, apply the transformation, and then enqueue the result.
There is also the dup
operation, which works on any datatype, popping off the element at the front of the queue and enqueuing it twice. This is useful for when you want to use something for calculation but then want to also be able to refer to it again.
Each of these operations by default operate on the current qframe, but they also have variants to operate on other queues. Prefixing any of these operations with r
causes it to instead refer to the register queue, and prefixing any of them with q
causes it to refer to the Queue object on the front of the current qframe
Sometimes one may want to simply remove something from a queue or transfer it to another queue. To do this, QQ offers some builtins for managing queues of all sorts.
rot
is a crucial operation for success. It pops from the front of the queue and then pushes that same element to the back, rotating the queue. Like the operations above, it has an r
and q
variant which operate on the register queue and Queue objects, respectively.
pop
behaves slightly differently depending on if it is used on the qframe. If it is, then it simply discards the element at the front of the qframe. Its variants instead pop the element from the front of the relevant queue and enqueue it on the qframe.
push
similarly varies depending on its target. In its default form, it is identical to rot
, rotating the qframe, but its primary utility is in moving things from the qframe onto another queue. rpush
and qpush
will take the element from the front of the qframe and push it onto the relevant queue.
drain
completely empties out the queue it is used on.
rqalloc
, mentioned above, takes a single argument and creates a register queue of that size. Note that you can only have one register queue at any given scope.
pack
is used to take several consecutive elements from the qframe and pack them together into a Queue object, which it then puts onto the back of the qframe. When using it, the element at the front serves to tell pack
how many elements to pack together, and then that many subsequent elements are used to form the queue.
exec
takes the element off the front of the qframe and executes it like a program. Note this only works on Queues. This is also an effective way to "unpack" a queue primarily used for storing data.
There are three primary ways to do control flow in QQ: if
, ifelse
, loop
, and the special rifbreak
.
if
is an operation that takes two elements off the queue. The first is the condition. The second is the consequent, which should be a Queue. If the condition is truthy, the consequent is executed by treating it as if it were itself a program (though in the current scope), executing each element in it one by one in order. In either case, the condition and the consequent are not re-enqueued.
ifelse
is very much like if
, but takes three elements. The first two are the same as if
, but the third is the alternate
, which is executed if the condition is falsey.
loop
takes a single element off the queue, which is the loop body. It is executed like the consequent above, but unconditionally. Additionally, as the queue is executed, each element is preserved and re-enqueued on the loop body, so that it gets executed again once the loop comes around. The only way to exit the loop is via either break
or rifbreak
rifbreak
is a special condition check, which takes its condition from the front of the register queue, and if true, breaks out of the current loop.
QQ provides some basic capabilities for defining and calling functions. Functions are unique from simple queues as code blocks in that they have their own qframe during execution. They are defined using def
and called using call
, and have the convenience of being the only thing stored outside of the queues described above, meaning they are accessible at any time.
def
takes two elements off the front of the qframe: the name of the function (which should be a String) and the body of the function (which should be a Queue). This body is then forever associated with the name.
call
takes two elements off of the front. The first should be the function name (again a string), and then a Queue that will become the qframe for that particular execution of the function. This queue can be filled with anything that the function expects to have as its initial qframe. After execution of the function, the callee's (probably mutated) qframe will be enqueued onto the caller's qframe.
A function terminates by either executing its final instruction or by executing the ret
instruction, which takes no arguments, since the return value is always the qframe.
QQ provides some basic IO operations: write
, print
, and QQ
.
write
and print
both print out the element at the front of the queue (without popping it off), but cannot access anything other than the frontmost element of the queue. They differ only in that print
adds an extra trailing newline that write
doesn't.
QQ
is a signal to the computer that you are now in tears. As a mercy, it prints the entire qframe and then terminates the program to spare you from further harm.
Currently, QQ is run by executing the evaluator.py
program with Python 3.9+. If given a filename afterwards, QQ will execute the whole file. Otherwise, it will put you into an interactive shell.
You can find some example programs in the test_programs
directory.