What is Heist?
Heist is a compiled, loosely invertible1 esoteric language (or esolang). The Heist compiler provided here utilizes an event-driven architecture, as opposed to the more conventional pipe-filter architecture seen in modern compilers.
As opposed to data streams which represent programs, Heist works with event streams, specifically those of keypresses. Whereas one may write print(3 + 4) in a language like Python, in Heist, one types the keypresses 3l4r+; to perform the same computation.
You can give it a spin here!
Why “Heist”?
The name was chosen as a play on the word hoist, alluding to the initial compilation strageties depending strongly on the esoteric JavaScript concept of hoisting.
How do I use Heist?
Here is a list of keypresses and their corresponding behavior. Unless otherwise noted, all keypresses behavior is invertible with a Backspace keystroke. By default, the compiler does not execute the code it generates. To evaluate the code, either copy-paste the compiled JavaScript code into your favorite JavaScript interpreter, or toggle “Enable Autoexecution” to see the computation performed as you type.
- 0 to 9 set the data register to the corresponding digit.
- + and a set the operator register to addition.
- - and s set the operator register to subtraction.
- * and m set the operator register to multiplication.
- / and d set the operator register to division.
- l sets the left argument register to the current value of the data register.
- r sets the right argument register to the current value of the data register.
- ; executes the current operation in the operator register with the current arguments in the left and right argument registers.
- Backspace reverts the most recently made operation.
How does Heist work?
The challenge of avoiding the conventional pipe-filter architecture in designing a compiler is significant. Even if a compiler has no explicit pipes or filters, it is trivial to, for example, draw the comparison between parsing a data stream (input file) as a basic pipe.
Enter Heist. The event-driven compiler is, as far as I can tell, a novelty in the field of compiler design. However, just as certain dubious chess openings are considered novelties, the novelty of event-driven compilers is less ground-breaking, and more sanity-questioning.
Event-driven architectures are quite useful for many applications, holding responsivity paramount. The crux of the Heist compiler boils down to a single event handler, a keypress listener. For each keypress, the listener emits a single "token" event which allows subscribers to record the corresponding compiled JavaScript output.
That's it. No pipes. No filters. Just two event generators and an event listener. Strictly speaking, the subscriber need not be embedded in a webpage, and even remote subscribers could reap the benefits from the Heist compiler.
The compiled JavaScript code
What about the compiled JavaScript code? By design, it isn't pretty, but it does work. Since we are working with individual keypress events, with no ability to anticipate future keypresses, the generated JavaScript code is quite messy. Although the base language supports a few registers, the ability to invert operations necessitates each of these registers be represented internally by a stack of registers. (I suppose you could also accomplish this by having a stack of program states. But that's less fun.)
All emitted instructions, with the exception of the undo keypress Backspace, takes the following form:
var history = history ?? [], stacks = stacks ?? {}, result; /* additional code; */ result;
Since I do not allow the compiler to emit header information (e.g. through an "init" event), there is initialization code for history, stacks, and a corresponding stack emitted before each instruction. The additional code handles initializing the individual stack and populating it with a value.
The most complex additional code with this template is that corresponding to the execute keypress ;:
var history = history ?? [], stacks = stacks ?? {}, result;
stacks['e'] ??= [];
history.push('de');
if(stacks.l && stacks.l.length && stacks.r && stacks.r.length) {
var l = stacks.l.pop(), r = stacks.r.pop(), o = stacks.o.pop();
stacks.e.push({ l, r, o });
stacks.d.push(result = o(l, r));
}
else {
stacks.e.push({}); stacks.d.push(undefined);
}
result;
Naturally, undoing an execution instruction requires restoring a lot of information, so the execution register stack holds entries from its three component input registers.
The Future of Heist
When I have more time, I'd like to hammer this language into something closer to turing complete. The concept has potential, as it is fairly generic; I imagine introducing first-order functions and defining useful operations on functions (like composition and iteration). I am unsure as of yet whether the planned functionality expanding in this direction will meaningfully interfere with the language's invertability.