Steampunk programming languages
Experiment with do-it-yourself steam-powered programming engines
An interpreter for a programming language such as VimanaCode can be viewed as an engine - a machine that processes instructions.
Vimana
VimanaCode (or simply Vimana) is a stack-based concatenative language, inspired by Forth and Lisp. A program in the language consists of a stream of instructions. The processing engine (the interpreter) is simplistic in its nature and reminds of a mechanical machine, like an old-time steam engine.
The engine uses a stack to processes the instruction stream. It feels a bit like a model railroad, with the train moving forward along the track as it computes the results produced by the program.
There is a certain "steampunk" feel to the language, and below we will discuss what that means.
A bit of background regarding the name of the language; Vimanas are flying machines mentioned in the Vedic literature. The texts describe advanced technologies that existed in ancient times. Were the Vimanas using some kind of computers, and if so, how were they programmed? Perhaps in an ancient programming language?
Code example
Two vital parts of the machine are the instruction stream (the program) and the data stack. The data stack holds the result of the computations produced by the interpreter.
The syntax is postfix, which means that the data elements precede the operations (functions).
Here is an example of a Vimana program. It consists of three number literals and four functions:
2 2 22 * swap - print
Initially the data stack is empty. After the machine having processed the first three instructions, the stack now contains these numbers (literal number are pushed directly to the stack):
22
2
2
Next instruction is the multiplication instruction, which pops the top two elements off the stack and pushes back the result. The stack is now:
44
2
The swap instruction shifts the order of the top two elements:
2
44
Next the machine performs the subtraction instruction, and the result is:
42
Finally, the print instruction pops the to element off the stack and displays it on the instrument panel.
Here are alternative ways to write the same computation (dup copies the topmost stack element):
2 dup 22 * swap - print
22 2 * 2 - print
The program is the steam that powers the engine. The instructions (functions) provide the mechanics of the machine, and the data stack holds the computed values (the moving parts).
The Steampunk movement
Steampunk is a scifi genre that blends high-tech and low-tech to create vivid fantasy worlds of the Victorian era. Steampunk features speculative, retro-futuristic technologies that might have been conceived in the 19th century, such as steam-powered machines and airships, as well as various mechanical clockwork-like devices designed in an elaborate style.
The Antikythera Mechanism, which is said to date back to around 200 BC, has the vibes of a genuine steampunk device. With its complex gear system consisting of many moving parts, it is an out-of-place artefact that might just as well have been designed in the 19th century.
Importantly, steampunk is a DIY (Do-It-Yourself) movement, where people write fiction, make movies, create models, art, clothing, accessories, and gadgets. There are cosplay events and gatherings where people dress in imaginative outfits.
Vimana is a steampunk programming language. It is DIY language, developed as a hobby project. The idea is that it should be doable for a dedicated hobbyist to implement an interpreter for the language, while keeping the code base small. Simplicity enables others to work on the code, and can inspire similar projects.
Steampunk is about sharing ideas and creations. It is like building a model railroad in your attic. You can do it for your own amusement, and sometimes for sharing your designs with others.
Steampunk programming languages
Typical of steampunk is the mix that blends high-tech and low-tech. Programming languages are commonly characterised as low-level or high-level. Assembly language and C are low-level languages, close to the workings of the underlying machine. Lisp and Smalltalk are high-level languages, providing programming constructs that abstracts away the physical machine. Then there is Forth, invented by Chuck Moore, which is a high-level language that is very close to the machine.
Forth interpreters are frequently implemented in assembly, and expose the underlying hardware architecture to a greater extent than C does. Still, it is in many ways a high-level language. In Forth, it is possible to create way more high-level abstractions than in C. Forth is in my view a true steampunk programming language. (For a discussion on low-level and high-level aspects of Forth, see pages 27-31 in Thinking Forth by Leo Brodie.)
VimanaCode provides several high-level abstractions, such as first-class functions and higher-order functions. At the same time, it is a low-level language. The parameter data stack is managed explicitly by the programmer – there is no automatic stack frame handling. Furthermore, there are no named local variables. Local state is stored on the data stack, which is more akin to programming with registers than variables. Such low-level traits require the programmer to pay explicit attention to details that are usually abstracted away by high-level languages.
Concatenative stack-based languages like Forth tend to be straightforward to implement. Implementing a basic Lisp interpreter is also relatively straightforward, but can quickly evolve into a complex task. For example, to fully implement Scheme is a big undertaking, because of the need to handle closures, stack frames and continuations, among other things.
Motivations for steampunk languages
One may wonder, what is the motivation for designing and implementing such odd and eccentric programming languages? Well, first of all, it is a fascinating and rewarding to create a programmable machine and see it in operation. There is also a strong aesthetic component, where the language syntax becomes an art form.
Contatenative stack-based interpreters have the advantage that they are quite straightforward to understand and implement. Not much code is needed to get something basic up and running. There are only a few concepts needed to create a stack-based language. Minimalistic design is a strong motivating factor.
To summarise, some design goals of VimanaCode are:
Minimalistic design – few language concepts
Simple implementation – small code base that is easy to understand and modify
Programming as an art form – clean and consistent syntax
A fun hobby project – rewarding to see your machine working
Steampunk syntax
We have talked about the blend of high-tech and low-tech as a hallmark of steampunk programming languages, but the aesthetics of the syntax also greatly contributes to the "look and feel" of the language.
Syntax can be "soft" or it can be "square".
Lisp
Let us begin with Lisp, which has a very uniform and minimalistic syntax.
Here is the recursive fibonacci function implemented in Common Lisp:
(defun fib (n)
(if (< n 2)
n
(+ (fib (- n 2)) (fib (- n 1)))))
Call the function and print the result:
(print (fib 37))
The rounded parens give a "soft" impression, that brings a sense of "elegance".
Forth
Now let us look at the same function written in GForth:
: fib ( n1 -- n2 )
dup 1 > if
dup 1- recurse swap 2 - recurse +
then ;
Call the function and display the result:
37 fib . cr
While the syntax of the above code may not be that hard to read, it is pretty cryptic and more "square" than Lisp. It has more of a "tech" look and is more "steampunk" (at least in my view :)
Vimana Lisp-like syntax
Vimana aims to be elegant, like Lisp, but even more minimalistic.
Here is the recursive fibonacci function implemented in the Lisp-like version of VimanaCode:
(fib)
(dup 1 >
(dup 1- fib swap 2- fib +)
ifTrue) def
Call the function and display the result:
37 fib print
Uniform postfix syntax
Every function call is strictly postfix and rounded parens are used to define lists (any list is a function).
Symbols in a list are quoted
Symbols in a list are "quoted" - a list is not evaluated until someone calls it as a function. This makes it possible to handle code and data in the same way. The language is "homoiconic". Lisp needs an explicit quote function, but in Vimana lists are always quoted, which makes the syntax very minimalistic (there is no need for special characters).
Implicit tail calls
This version of Vimana has implicit tail calls. If a function is the last one in the list being evaluated, the current frame on the call stack is reused – thus the call stack does not grow unnecessarily. In the above example, "ifTrue" will not create a new stack frame, because it is last in the code list.
Vimana uses tail recursion for loops, it does not have a loop construct built into the language. The programmer can create her own loop functions by defining higher-order functions.
Defining functions
A new function is defined with def. It takes two parameters o the stack; a function name (a symbol in a list), and a list with the function body. Here is an example:
(add10) (10 +) def
The name of the function must be in a list, or it would have been interpreted as a function call. List are pushed to the data stack, they are not evaluated like in Lisp.
The data stack looks like this when def is called:
(10 +)
(add10)
We can call our new function like this:
32 add10 print
Evaluate data as code
Code in a list is evaluated with the eval function. Here is an example:
(32 add10 print) eval
VimanaSteam – steampunk syntax
I have been thinking a lot about the syntax for Vimana, and experimented with many alternatives.
Perhaps the Lisp-look with rounded parens is not really in the spirit of the language? Square brackets could give the code a more machine-like "steampunk" look.
Here is the recursive fibonacci function implemented in the most recent version of Vimana, called "VimanaSteam":
[fib]
[A 1 isSmaller ifTrueTail
[A 1- fib swap 2- fib +]] :
Call the function and print the result:
37 fib print
Syntax changes
Square brackets are intended to provide a more "tech-like" look.
The colon character is used to define a function (same as in Forth, but postfix).
Infix functions and explicit tail calls
The function ifTrueTail is an infix function. This is a step away from the minimalistic style of using only postfix functions. A reson for this is performance; infix conditional functions are faster.
Furthermore, tail calls are explicit in VimanaSteam. This provides greater flexibility and more control to the programmer, in the "low-level" steampunk spirit. It also becomes clear when tail calls are used. Tail calls can make the code more efficient.
Function names
The function A is used in place of dup (copies the first stack element). There are also functions B and C. These functions copy elements down the stack to the top of the stack. B copies the second stack element (corresponds to over in Forth). C copies the third stack element (not available in Forth as a standard function). The idea here is to see if this is a more logical and useful naming pattern compared to dup and over.
The idea with the function isSmaller is that greater-than and less-than signs do not go that well with a postfix syntax.
In the above fib function, A puts a copy of the topmost stack element on the stack; then 1 is pushed to the stack. isSmaller tests if the element at the top of the stack is smaller than the element below it.
Stack example:
1
37
37
isSmaller is called, and the stack now contains:
0
37
In Vimana 0 is false and everything else is true.
Prefix functions and quoting
As an alternative to the postfix def (":") function, a prefix version would be possible:
: fib
[A 1 isSmaller ifTrueTail
[A 1- fib swap 2- fib +]]
In this case, the function symbol does not need to be inside a list, since it is not evaluated by the colon function.
Also note that the function body is treated as a literal when using this style, which means that it cannot be dynamically computed (will elaborate on this in a future article).
Just a quick example to show what I mean; this kind of dynamic code generation would not be possible with a prefix syntax (cons works as in Lisp, but with a postfix syntax):
[makeadder] [[+] cons] :
[add10] 10 makeadder :
32 add10 print
Summary of alternative styles
The above code examples show that even with a minimalistic programming language, the possible syntax styles are vast.
Below a number of alternative examples are listed. Which ones do you prefer and why?
Vimana style ("Lisp-like")
Original style (postfix functions only):
(fib)
(dup 1 >
(dup 1- fib swap 2- fib +) ifTrue) def
(fib)
(dup 1 >
(dup 1- fib swap 2- fib +) ifTrue) :
Prefix and infix functions:
def fib
(dup 1 > ifTrue
(dup 1- fib swap 2- fib +))
: fib
(dup 1 > ifTrue
(dup 1- fib swap 2- fib +))
VimanaSteam style ("steampunk")
Postfix functions only:
[fib]
[A 1 isSmaller
[A 1- fib swap 2- fib +] ifTrue] :
[fib]
[A 1 isSmaller
[A 1- fib swap 2- fib +] ifTrue] def
Prefix and infix functions:
: fib
[A 1 isSmaller ifTrueTail
[A 1- fib swap 2- fib +]]
def fib
[A 1 isSmaller ifTrueTail
[A 1- fib swap 2- fib +]]
What do you think?
What do you think about these examples? Are there other syntax variations that you would consider?
I should point out that line breaks and indentation are not significant in VimanaCode. Any whitespace character can be used to separate symbols and numbers.
Next up is a Javascript version of VimanaCode, which I am working on. It will run in the web browser. But which syntax should be preferred? Lisp-style or Steampunk?
Images AI-generated by the author.