Lift - A small stack-based programming language

Tags: technology

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