Lift - A small stack-based programming language
A few months ago I worked on a small stack-based programming language. In short, it’s a weird mix of Forth and PostScript. I named it Lift because it sounds like a lightweight word.
I wrote it in Javascript for mainly two reasons:
- I could easily embed it in a website
- It does most of the low level work. The language is less than 500 lines of plain JS.
I chose a Forth-like language because parsing it is very simple.
I had a lot of fun writing it, but so far I haven’t found a good use case for it. At the end of this post is a small console where you can play with it.
The basics
If you know Forth, you already know the basics of Lift. Everything revolves around the stack.
# Basic Arithmetic
3 4 + .
# Stack operations (dup, drop, swap, over)
4 dup .
# Defining new words
: squared dup * ;
6 squared .
More than just numbers
The similarities end here, for the most part. Lift’s stack not only stores numbers, but also strings and arrays.
[ 1 2 3 4 ] # Push an array to the stack
len . cr # Print its length
[ 1 2 3 [ 4 5 6 ] ] . cr # Nested arrays are allowed
"Hello, world!" . cr
"join strings " "like this!" + . cr
[1 2 9 4] (A) # Assign the array to 'A'. More on this feature in the next section
A 2 get # push A[2]
A len # push A.length
A 2 3 put # A[2] = 3, push A
Destructuring
This is a useful feature of the language. Manipulating the stack can be cumbersome, so I came up with this special operation to pop N values from the stack and store them in variables for later use. “Destructuring” was the best word I could find for it. I guess you are destructuring the stack into a set of named values.
1 3 (a b) # Stores 1 in a, and 3 in b
a b + . cr # We can use the values like any other word
# We can define new words using this syntax
: reverse (a b c) c b a ;
# All basic stack operations can also be redefined
: dup (x) x x ;
: swap (a b) b a ;
: drop (a) ;
: over (a b) a b a ;
Combining the destructuring syntax with the word definition syntax resembles mathematical functions a lot!
Looping and branching
You can store words and constants in the stack without executing them using
curly braces. These work like anonymous words i.e. words without a name in the dictionary.
if
and ifelse
are used to add conditionals.
20 {+} 3
swap exec
. cr
5 (a)
a 4 >
{ "a is bigger than 4" . cr }
{ "a is not bigger than 4" . cr }
ifelse
This way you can also create loops
10 { "Hello!" . cr } repeat
0 10 1 { . cr } for
Functional programming
Finally, a few useful words to work with arrays
[1 2 3 4 5 6 7 8 9] (numbers)
numbers { dup * } map . cr # Print 'numbers' squared
0 numbers {+} foreach . cr # Print the sum of 'numbers'
numbers { 2 mod 0 = } filter . cr # Print only the even numbers
More examples
A range
word that creates an array of numbers from 0 to N. This takes
advantage of the for
word being able to push values to the stack. The
opening square bracket is a token that gets pushed to the stack, and the
closing bracket pops values from it until it finds its match.
: range (N) [ 0 N 1 {} for ] ;
4 range . cr
10 range . cr
Destructuring is also useful for naming arguments inside complex functions, improving readability and conciseness.
: discriminant (a b c) b b * 4 a c * * - ;
Because the tokens for building arrays are regular words, you can do weird stuff like this:
: BEGIN_ARRAY [ ;
: END_ARRAY ] ;
BEGIN_ARRAY 1 2 3 4 END_ARRAY
Finally, a recursive function to get the factorial of a number.
: factorial (n) n 2 < {1} {n n 1 - factorial *} ifelse ;
5 factorial . cr
4 factorial . cr
3 factorial factorial . cr
A Lift interpreter
You can view the full source of the language. I haven’t worked on a full documentation for it, as I currently don’t have a need for it.
You can try out the examples I showed above.