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
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.
| |
Recursion replaces looping, and let ... in supplies local bindings:
| |
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:
| |
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:
| |
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:
| |
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:
| Group | Instructions |
|---|---|
| Stack | push n, dup, copy n, swap, pop, slide n |
| Arithmetic | add, sub, mul, div, mod |
| Heap | store, load |
| Flow | label x, call x, jump x, jz x, jn x, ret, end |
| I/O | pc, pn, rc, rn |
This is what makes the “only two built-ins” claim true rather than rhetorical. Addition really is defined in the language:
| |
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:
| |
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
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