Est. 2009 Intermediate

HaPyLi

A small Lisp-flavoured functional language whose only backend is Whitespace, written by a programmer with nothing to do at work, and one of the rare high-level languages built for an esoteric target that were actually used to write real programs.

Created by Cybis, a pseudonymous developer who signed the 2010 mailing-list announcement as "Cybis FDP". No real name is given on the original homepage, in the source archive, or in the Esolang wiki entry, and none is asserted here

Paradigm Functional in shape rather than in theory. Every function body is a single expression, functions take arguments by value and return exactly one value, parameters and let-bound locals are read-only, and iteration is written as recursion. It is not purely functional: global variables and heap arrays are mutable through explicit ref and set calls, and there are no closures, no higher-order functions and no lambda
Typing Typeless. The tutorial states it directly: every expression evaluates to an integer, and it is up to the programmer to remember what a given function takes and returns. Strings are null-terminated integer arrays in the heap and a string literal evaluates to its heap address
First Appeared 2009. The recovered source history opens with a commit dated 25 January 2009, and the compiler and tutorial were not published until the language was announced on the Whitespace mailing list on 23 May 2010
Latest Version Unversioned. The author branched the Subversion trunk to "v1.0" on 9 April 2009 but never published a numbered release; the compiler and tutorial are distributed only as a source tree. The recovered archive last received a code change on 31 October 2023

HaPyLi is a small, typeless, Lisp-shaped functional language with exactly one backend: Whitespace, the esoteric language whose entire syntax is spaces, tabs and line feeds. The name is an acronym of its stated influences - Haskell, Python, Lisp - and its purpose was to make Whitespace’s virtual machine programmable by a human being. It is worth attention for two reasons that have nothing to do with novelty. First, unlike many high-level frontends written for an esoteric target, it was actually used: a working sudoku solver and a Brainfuck interpreter exist in it. Second, it is a compact and honest case study in how a compilation target reaches up through a language design and rearranges it.

The origin story is documented by the author himself, on the original homepage, under the heading Umm… what the hell? Why?:

Have you ever been told by your boss to look busy, even though there were no projects for you to do? Now imagine being told that, roughly every day, for two years. That’s what it’s like to be a programmer for a certain United States government agency. HaPyLi is the end result of my “looking busy”.

History

Development starts on 25 January 2009. The first commits in the recovered repository are Haskell files - Lexer.hs, Parser.hs, Ast.hs, Validator.hs, main.hs - because the project was partly an excuse to learn Haskell, Python, regular expressions and compiler construction at once. That version does not survive to a release. On 9 April 2009 the author branches the trunk to v1.0 and begins again in Python, explaining afterwards that Haskell “felt too clunky” and that he had not yet mastered proper abstraction in functional languages. It is a small irony worth noting: the compiler for the functional language is the thing that got rewritten out of the functional language.

The Python compiler comes together fast. The lexer, lexeme parser, AST parser and Whitespace emitter are all working within about three weeks; a commit on 12 April reads simply “Wow, I think it’s finished.” The standard library follows at the end of April, and by 10 May 2009 the sudoku solver runs.

Then nothing, for a year. The tutorial commit of 7 May 2010 opens with “After nearly a full year, I’m finally writing a tutorial for HaPyLi”, and it happened because a friend suggested it. The public announcement is one paragraph sent to the Whitespace mailing list on 23 May 2010, pointing at hapyli.webs.com and a compiler requiring Python 2.5. Two contributed programs arrive that November and December, and that is essentially the whole public life of the language.

The site went offline and the code was lost - the Esolang wiki puts this some time between 2013 and 2015, though the Internet Archive’s last capture of the homepage is from September 2012. In May 2023, Thalia Archibald - assembling a corpus of Whitespace tooling - obtained a copy from the author, ported the compiler to Python 3 and republished the tutorial. The Subversion history came with it, which is why the dates above are commit dates rather than guesses.

Design

Everything is one expression

A HaPyLi program is a set of defs, each of whose body is a single expression. There are no statements.

1
2
3
4
5
import "stdlib/base.hpl"

def main() = (print-string "Hello, world!")

; Comments start with a semicolon, as in Lisp.

Recursion replaces looping, and let ... in supplies local bindings:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def power(x y) =
    (if (== y 1)
        x
        (* x (power x (- y 1))))

def main() =
    let
        x = (read-number)
        y = (read-number)
    in
        (print-number (power x y))

Parameters and let-bound locals are read-only - the tutorial is explicit that their values cannot change during a function’s execution. Function names are permissive enough to include ~ ! @ # $ % ^ & * \ - _ + | : , / ? along with letters and digits, which is why print-string, ++ and == are ordinary identifiers rather than special syntax.

Two built-ins, and only two

The tutorial’s central claim is that if and do are the only expressions the compiler knows about. Everything else, arithmetic included, is defined in HaPyLi in stdlib/base.hpl.

if exists because it is the only form that short-circuits: normal calls evaluate all arguments left to right before the call happens, so without it there is no control flow. Being typeless, it treats 0 as false and anything else as true, with the tutorial’s warning that some library functions assume true is specifically 1.

do exists because sequencing otherwise requires a variadic function, which the language cannot express. The tutorial shows the workaround it replaces - defining execute at two, three and four parameters and so on - before conceding that do is simply built in and accepts any number of arguments, returning the value of the last:

1
2
3
def prompt() =
    (do (print-string "Enter your favorite number: ")
        (read-number))

Overloading by arity

Two functions are distinct if their names differ or their parameter counts differ. This is how - serves as both binary subtraction and unary negation in the standard library, and how a user can define f(a b) and f(a b c) side by side.

Values, strings and the heap

Every expression is an integer. Numeric literals come in decimal, hexadecimal and character forms - 65, 0x41 and 'A' are the same value. String literals are compiled to null-terminated integer arrays placed in Whitespace’s heap, and the literal evaluates to the address of that array, which is why print-string is a recursion over ref:

1
2
3
4
5
6
7
8
9
def print-string (*str) =
let
    c = (ref *str)
in
    (if c
        (do (print-char c)
            (print-string (++ *str))
            *str)
        *str)

Global variables live in the heap and are read and written through ref and set; the leading * in names like *A is only the author’s pointer convention, not syntax. There is no bounds checking whatsoever - the tutorial warns that writing past the end of an array may silently overwrite a different variable, and that reading from an address never written to can crash the Whitespace interpreter. An alloc exists, modelled on C’s malloc; there is no free, and the tutorial says so plainly: “Sorry, I never wrote one. The HaPyLi Standard Library is unfinished.”

The Target Reaches Up

Two of HaPyLi’s most distinctive features exist purely because of what it compiles to.

inline, because calls are expensive

Whitespace’s call instruction takes a label, and finding that label means scanning the program. The tutorial states the consequence directly: for each call instruction, Whitespace searches every instruction in the entire program for the matching label. HaPyLi’s answer is a keyword:

1
def inline sum (a b) = (+ a b)

An inlined function emits no call; its body is copied to each use site. Recursive functions cannot be inlined. The author’s own assessment of when to use it is refreshingly unglamorous - he does not know of any method for deciding, notes that inlining always grows the compiled program, and observes that it may therefore either help or hurt overall performance. No measurement of the trade-off was ever published, and none should be inferred from the keyword’s existence.

asm, because the standard library has to bootstrap itself

A function may be written in an embedded Whitespace assembler by using asm in place of def. The assembler is a readable one-to-one mnemonic layer over the Whitespace instruction set:

GroupInstructions
Stackpush n, dup, copy n, swap, pop, slide n
Arithmeticadd, sub, mul, div, mod
Heapstore, load
Flowlabel x, call x, jump x, jz x, jn x, ret, end
I/Opc, pn, rc, rn

This is what makes the “only two built-ins” claim true rather than rhetorical. Addition really is defined in the language:

1
2
3
asm inline + (x y) = ( add )
asm inline not (x) = ( push 1 swap sub )
asm inline print-number (n) = ( dup pn )

Assembler functions are otherwise ordinary: they can be inlined, can contain let-forms, can call and be called by def functions, and always return a value. Parameters and locals are reached with copy n, indexed from 0 in reverse declaration order, and the function must leave exactly one value on the stack. The compiler supplies the trailing ret and the program’s final end - the tutorial’s counting example is the Whitespace tutorial’s own program with those two instructions deleted for exactly that reason. The manual’s own capitalised warning is that it is very easy to corrupt the stack this way; a language that hands you the machine also hands you the machine’s failure modes.

Using It Today

The compiler is a Python source tree run as python main.py, with the standard library imported by path:

1
import "stdlib/base.hpl"

The 2023 port means it runs on Python 3; the original required Python 2.5. There is no installer, no package, no Docker image and no version number - you clone the archive or you do not have it. Output is a .ws file for any Whitespace interpreter.

For a rough sense of what compilation costs in size, the archive ships sources next to their compiled output: 99bottles.hpl is about 1.2 KB of HaPyLi and its 99bottles.ws about 6.5 KB, while the 5 KB sudoku.hpl produces about 13.5 KB of Whitespace. These are simply the file sizes of the checked-in artifacts - the ratio is largely an artefact of the inlined standard library being copied into the output, and no runtime or throughput measurement of HaPyLi programs has ever been published.

Why It Matters

HaPyLi is a small language with a short public life and no descendants, and it earns its place for reasons other than influence.

It is an unusually clean demonstration that an esoteric target does not require an esoteric source language. Whitespace is a joke about syntax; the machine underneath it - stack, heap, arithmetic, labelled calls, four I/O instructions - is a perfectly ordinary one, and once someone builds a real compiler for it, real programs follow. The sudoku solver and the Brainfuck interpreter are the proof, and they are what separate HaPyLi from the many toy frontends that stop at Hello World.

It is also a good illustration of how far a target’s cost model propagates upward. inline and asm are not general language design ideas; they are direct responses to a linear label search and to the absence of primitives, promoted into keywords. Read in that light the language is a compressed lesson in why real compilers grow the features they grow.

And it is a case study in software preservation. The site died, the code was lost for the better part of a decade, and the language survived only because someone building a corpus asked the author for a copy - and because the author still had one, complete with its 2009 revision history. Most languages of this size that vanish stay vanished. That HaPyLi’s commit log can be read today, message by cranky message, from [r1] to “Sudoku solver is working perfectly!”, is the exception rather than the rule.

Timeline

2009
Work begins on 25 January. The earliest commits in the recovered repository are a Haskell compiler - Lexer.hs, Parser.hs, Ast.hs, Validator.hs - and the author later explained the choice on his homepage: he built the language partly to learn about text parsers, regular expressions, compilers, Haskell and Python
2009
On 9 April the trunk is branched to v1.0 and the compiler is restarted in Python. The author's stated reason is that Haskell "felt too clunky" and that he had not yet mastered proper abstraction in functional languages. The Python compiler - the HplLexer, HplLexemeParser, HplAstParser and whitespace emitter that survive today - is written over roughly three weeks in April
2009
In late April the standard library takes its present shape. The 27 April commit is labelled "HaPyLi Standard Library / Version 2", so an earlier version already existed; that commit and the one on 28 April add arithmetic, comparison and logic functions, and by 29 April the file is reorganised as stdlib/base.hpl. Almost every operator in it is written as an inline Whitespace assembler function inside HaPyLi itself, which is what lets the compiler stay small
2009
On 10 May the author records that the sudoku solver is "working perfectly", after a commit four days earlier complaining that it "works" apart from out-of-memory errors. The solver is roughly 150 lines of HaPyLi and remains the largest program written in the language
2009
On 19 May the solver is compiled to Whitespace. The reference attached to that commit in the recovered archive points at a Daily WTF forum thread, which would make the compiled output the earliest public trace of the project; the thread is no longer readable without an account, and the language and compiler themselves stayed unpublished for another year
2010
On 7 May, after what the commit message calls "nearly a full year", the author starts writing the tutorial. It is converted to HTML on 19 May and given a navigation bar and downloadable sample files the same day
2010
On 23 May the language and its compiler are announced publicly for the first time, in a single short message to the Whitespace mailing list, signed "Cybis FDP": "About a year ago, I wrote a sudoku solver in a LISP-like language I designed to compile to Whitespace. A friend recently suggested that I should post the compiler and write up a web tutorial for that language." The site is hapyli.webs.com and the compiler requires Python 2.5
2010
The only outside contributions arrive at the end of the year. Marinus Oosters writes a 99 Bottles of Beer program in HaPyLi on 27 November and a Brainfuck interpreter on 1 December; the author lists the 99 Bottles program under contributions on 18 December. The Brainfuck interpreter is the clearest demonstration that the language is capable of ordinary work
2013
The HaPyLi website goes offline and the source code is lost. The Esolang wiki dates this to some time between 2013 and 2015; the Internet Archive holds only three captures of the homepage, the last of them 5 September 2012, so the exact date is not recoverable
2023
The language is recovered. Thalia Archibald, working on the wspace Whitespace corpus, obtains a copy from the author, ports the compiler to Python 3 on 15 May, reorganises the tree and republishes the tutorial on 16 May, and makes a final parser fix on 31 October allowing comments to terminate tokens. Crucially the original Subversion history came with it, so the 2009 revisions r1 onward are preserved rather than reconstructed
2026
As of August the archive at github.com/wspace/cybis-hapyli and the republished tutorial remain online and unchanged since 2023. There is no active development, no issue traffic of note and no versioned release - the language is preserved rather than maintained

Notable Uses & Legacy

The sudoku solver

The program that caused the language to exist in publishable form. Written in 2009 and finished on 10 May of that year, it is about 150 lines of HaPyLi and ships in the archive alongside its compiled Whitespace output. The author's own announcement leads with it rather than with the compiler, and it is the strongest evidence that HaPyLi was a working tool and not a syntax sketch

Brainfuck interpreter

Contributed by Marinus Oosters on 1 December 2010 and included in the archive as programs/brainfuck.hpl. An interpreter for another language, compiled down to Whitespace, is a meaningful demonstration of the heap, array and recursion facilities working together - it needs a program tape, a data tape and a dispatch loop, none of which the tutorial's examples exercise

99 Bottles of Beer

Also by Marinus Oosters, written 27 November 2010 and listed by the author under contributions that December. HaPyLi has an entry in the 99 Bottles of Beer collection, which along with the Esolang wiki is one of the few places the language appears outside its own repository

The HaPyLi standard library

stdlib/base.hpl and stdlib/list.hpl are themselves the most instructive use of the language, because their primitive layer is written in HaPyLi's embedded Whitespace assembler. Addition is one add instruction, comparison is a jn and two pushes, print-string is ordinary recursion over a null-terminated array. The library is where the language's design claim - that only if and do are built in - is actually cashed out

The wspace Whitespace corpus

HaPyLi is archived as part of a broader effort to collect the interpreters, compilers and programs of the Whitespace ecosystem. Its role there is as one of the ecosystem's few high-level frontends: most Whitespace tooling is interpreters and assemblers, and a compiler for a designed source language with a standard library is unusual

Language Influence

Influenced By

Running Today

Run examples using the official Docker image:

docker pull
Last updated: