<=====------======------===[ On ~ATH ]===------======------=====>
~ATH is an esoteric programming language that was conceptualized
for the Homestuck webcomic (don't worry, this post is not about
Homestuck). Pronounced "til death", the language's primary
method of executing code relies upon the death of real-world
entities. The language exclusively consists of infinite loops
(which usually do not contain much in the way of actual
instructions), and the only way to make these loops terminate is
by tying them to the lifespan of some entity with a finite life.
Only after the loop, in an EXECUTE statement, can truly useful
code be run. In Homestuck, the entities that can be tied to
loops in this way consist exclusively of real things that are
generally quite long-lived: one of the shortest lifespans a loop
can be tied to is that of its author. Of course, this makes the
language both near-useless without significant trickery within
the comic's own fiction, and also impossible to implement in
real life.
So when I made my implementation in Python, I basically made a
bunch of stuff up.
Now, before I start talking about the decisions I made in my
implementation, I want to make it clear that I am far from the
first person to do this. There are many other implementations of
~ATH, each with its own ideas on adapting the language's rules
to the real world that are just as interesting as my own. In
particular, drocta ~ATH is one of the oldest implementations I
know of and a large source of inspiration for how mine works
(really, there are only a handful of meaningful differences
between mine and theirs, but they affect the language quite a
bit). In the links section to my phlog I'll leave a few relevant
links about drocta ~ATH and a few other implementations I found
interesting while doing research for this one. I definitely
recommend checking them out if you're interested. With that out
of the way, onto the details of my implementation.
Thinking about how to maintain the core concept of the language,
I eventually decided upon having loops tied to abstract objects
that can be killed at-will by the user. To be clear, these are
not objects in the OOP sense (except under the hood of the
interpreter, but that's neither here nor there), but are rather
representations of something out there in the world that is
either alive or dead. Whether an object is alive or dead is the
only state it contains, and the only way to transform an object
is by killing it with OBJECT.DIE(). The .DIE() function
exists within the comic, but is only ever shown to be used to
end the program by calling THIS.DIE() (a function that it will
continue to perform in my implementation). You create a
reference to an object with the following syntax:
__________________________________________________________
| |
| import abstract VARNAME; |
|__________________________________________________________|
The object will be alive upon creation. Note that we will be
making use of this syntax to import things that aren't abstracts
later. With this, we can now create and kill abstract objects
with otherwise indefinite lifespans, but how do we actually USE
that? And thus the language's primary (and only) control flow
structure enters the scene: the ~ATH loop. It's declared like
this:
__________________________________________________________
| |
| ~ATH(VARNAME) { |
| [THING1] |
| } EXECUTE([THING2]); |
|__________________________________________________________|
This can be read as "Til death of VARNAME, do THING1. Then, do
THING2." THING1 will be repeatedly executed, checking each
iteration if THING1 is still alive. If it isn't, the loop stops
executing, and THING2 is executed once. Notably, if VARNAME is
already dead, the loop AND the EXECUTE are skipped. Also, the
EXECUTE statement is a mandatory part of the loop. It cannot be
empty, but can contain the keyword NULL if you do not want to
execute anything. Now, when you take all of this in combination
with what we've already established, it's not hard to see that
the central challenge of writing ~ATH as it is presented in the
comic has been removed. The entire point of ~ATH in Homestuck is
that it's very difficult to get an EXECUTE statement to execute
within any reasonable time frame without significant difficulty.
In this version of ~ATH, one can trivially trigger an EXECUTE
with the following pattern:
__________________________________________________________
| |
| import abstract LAMB; |
| ~ATH(LAMB) { |
| [THING1] |
| LAMB.DIE(); |
| } EXECUTE([THING2]); |
|__________________________________________________________|
This pattern is also helpful for getting any ~ATH loop to
execute precisely once.
| LAMB is one of two variable names I repeatedly use
| as a matter of convention in order to describe a common
| variable purpose. A LAMB is any object that is created for
| the explicit purpose of being almost immediately killed,
| usually to trigger an EXECUTE or have a loop that executes
| once, as above.
Now, at this point, we're pretty close to having all the
syntax that ~ATH has in Homestuck defined. Unfortunately, the
language with only the above components is also completely
impossible to do anything interesting in. This is where the last
major piece of the language comes into play: bifurcation.
Bifurcation will not only make it possible to write more complex
programs, it also introduces what the central challenge of
writing code in my ~ATH implementation is (if it weren't
endlessly frustrating to use, it wouldn't be ~ATH).
In Homestuck, we see the following syntax:
__________________________________________________________
| |
| bifurcate VAR[VAR1, VAR2]; |
|__________________________________________________________|
Within the comic this is exclusively used in one notorious
script that that splits itself into two halves by doing
bifurcate THIS[THIS, THIS] (each half of THIS was written in a
different color). My implementation does not support this
behavior for what I hope are obvious reasons. Instead, bifurcate
can be used to get each half of an object. I say "get" instead
of "create" because sometimes (often) you are bifurcating an
object that you have already previously bifurcated. In such a
case, VAR1 and VAR2 would become duplicates of the already
existing halves. Duplicates and not references because, if you
were to kill VAR1, the original version would still live. In
other words, bifurcation passes by value, not reference.
It is this property that allows us to create useful programs.
| It is VERY IMPORTANT to keep in mind the first time you
| bifurcate an object, you are actually *creating* the two
| halves, and the two variables you produce act like the
| "master copies" of those objects. If you kill them, all
| future bifurcations of their parent object will produce
| duplicates of the dead halves. Sometimes this is wanted,
| and sometimes it is not.
The last rule of bifurcation is that an object can have two
dead halves and still be alive, but when an object dies both of
its halves die as well. Think of the two halves as children on
a binary tree, and death as a state change that can propogate up
the tree but not down it. And now we see the fundamental
challenge this version of ~ATH presents: how do you write
programs in a world where your only data structure is a binary
tree that can only be traversed in one direction, and which
consists of nodes whose only state is whether they are alive or
dead (which is a one-way mutation). Plus, your only control flow
is the ability to loop over a block of code until one of these
nodes becomes dead.
Challenge accepted, I said, to this problem I had just invented.
But first, I needed to determine how I would handle textual
input and output. What I ended up deciding upon was very simple
(and largely borrowed from drocta ~ATH), so I'll explain it
quickly: "input" is a new type of object you can import (as in,
import input VARNAME). When the input declaration is processed
by the interpreter, the user is prompted for input and can type
in whatever they would like before pressing Enter.
The entire line of input text is stored in the input object, but
there's no way to actually access it. Instead, when you
bifurcate the object, the left half takes the first character,
and the right half takes the rest of the input. An input object
is alive unless it only contains an empty string or a zero.
In this way, we can process binary input by testing whether each
individual bit is a 1 or 0 (alive or dead). Of course, the user
could input any non-zero character in place of a 1, but that
doesn't affect us at all.
For output there's simply a PRINT keyword that has to be
followed by a string of text enclosed in double quotes, which
prints the string to stdout followed by a newline. PRINT
statements can only be placed inside of EXECUTE statements. This
means that it is common to have a lot of LAMB patterns in your
code for the sole purpose of printing something to the console.
Of course, this is really more of an annoyance than anything,
which is a net positive as far as this language is concerned.
When it can't be frustrating, it'll do its best to be annoying.
Once I had my plans for the language in mind and had created a
grammar for it, I set about creating the Python interpreter.
I started this having never written Python before and with very
little idea how interpreters work. However, Python is a very
easy language to pick up while you build something, and the
subject of interpreters proved to be less complex than I
expected (in terms of the very basics, which is all I really
needed to know for this project). So I hacked the thing together
in three days. On day one I didn't spend a huge amount of time
on it, but I made a very simple tokenizer with some odd quirks
based on similar simple python tokenizers I had found. On day 2
I read up a whole bunch on recursive descent parsing, which
turned out to be a lot of fun, and the parser was pretty
relaxing to write all things considered. On day 3 I finished the
parser and did the entire actual interpreter.
The result of how I made my ~ATH interpreter is that its a
hacked together mess with a lot of odd quirks and extremely
unhelpful error messages. It was everything I needed from it,
though, since it was just for myself to play around with. This
does mean, however, that I am not going to share the interpreter
on here. I do plan on someday rewriting the implementation,
likely in an entirely different language, and if I do I'll
make another post about it and provide the interpreter alongside
some of my own ~ATH code. For now, I'll put the (EBNF) grammar
in the links for anyone who's interested.
I don't want to end this post without giving a real example of
how one DOES anything in my version of ~ATH, so I'm going to
describe how I implemented a mimicry of integers within the
language. Hopefully it'll help you understand how one utilizes
the bifurcate keyword and the structure that it presents.
To create a method of interacting with numbers within ~ATH, I
constructed a particular tree structure in which each node (or
abstract object, in ~ATH terms) represented a singular number.
For each number, the left half of the number is the number
beneath it. The right half of the number is irrelevant. The left
half of the object that represents 1 is dead, representing 0.
All in all, the tree looks like this (right halves removed for
clarity):
__________________________________________________________
| |
| ROOT |
| | |
| N255 |
| | |
| N254 |
| . |
| . |
| . |
| N2 |
| | |
| N1 |
| | |
| N0 <-- DEAD |
|__________________________________________________________|
I only created the numbers up to 255 so as to allow for basic
8-bit input and output in my programs. Creating this structure
is a hassle but not difficult (continuing ~ATH's goal of
being annoying), as it just consisted of importing an abstract
called ROOT, bifurcating it and calling the left half N255,
bifurcating that and calling the left half N254... all the way
down to N0, and then calling N0.DIE(). I ended up writing a
python script to automatically generate that ~ATH code for me
when given how many numbers I wanted to have in my "tree." Then
I saved the output to NUMBERS.~ATH and imported it to my other
~ATH programs as such:
__________________________________________________________
| |
| import library NUMBERS; |
|__________________________________________________________|
| The library import type can only be placed at the top of a
| program, before any non-import statements. It quite simply
| takes the listed file and interprets it before continuing
| with the current file. Ideally this is only used to create
| helpful variable references as I've done here, but since
| I made the interpreter for myself alone I never bothered to
| make it enforce that as a rule.
Now I can do stuff with numbers! As an example, here's how you
loop over a block of code any given number of times (in this
case, 6 times) and then exit:
__________________________________________________________
| |
| import library NUMBERS; |
| bifurcate N7[I, JUNK]; |
| ~ATH(I) { |
| # Loop body goes here |
| bifurcate I[I, JUNK]; |
| } EXECUTE (NULL); |
| THIS.DIE(); |
|__________________________________________________________|
To create a reference to 6, you have to take the left half of
seven, becuase killing N6 directly would do BAD things (namely
shift the whole number line down 6, since 6 would become the new
zero). Then in each iteration of the loop, you are able to
change the value of I to the value of its left half, effectively
decrementing it until it "dies" by reaching zero. In both of the
bifurcation statements above, the right half is thrown into a
JUNK variable as it is not needed. In my own code, I tend to use
the name NUL for this purpose, but I'll use JUNK in this post to
avoid confusion with the NULL keyword.
| Every program in ~ATH has to end with THIS.DIE(), but it can
| also be used to terminate the program early.
Now, if you want to subtract a number Y from a number X, that's
relatively easy. If you take the above code but also decrement X
(let's say 12 in this case) in each loop, then at the end X will
contain the result of the subtraction:
__________________________________________________________
| |
| import library NUMBERS; |
| bifurcate N13[X, JUNK]; |
| bifurcate N7[Y, JUNK]; |
| ~ATH(Y) { |
| bifurcate X[X, JUNK]; |
| bifurcate Y[Y, JUNK]; |
| } EXECUTE (NULL); |
| # X now contains the same value as N6, and Y is dead. |
| THIS.DIE(); |
|__________________________________________________________|
You may be wondering how its possible to increment numbers,
since the bifurcation tree can only be traversed downwards (in
other words, you can't use two halves to get the value of the
thing they are halves of). Luckily, a combination of two things
will make incrementing (and addition) possible:
- We still have the ROOT value at the top of our tree
- a + b = c - (c - a - b)
With that in mind, here's how you would add 1 to a number X:
__________________________________________________________
| |
| import library NUMBERS; |
| bifurcate N6[X, JUNK]; |
| bifurcate ROOT[TEMP, JUNK]; |
| ~ATH(X) { |
| bifurcate X[X, JUNK]; |
| bifurcate TEMP[TEMP, JUNK]; |
| } EXECUTE (NULL); |
| # TEMP now contains 255 - 5 = 250 |
| |
| bifurcate TEMP[TEMP, JUNK]; # Now it contains 249 |
| |
| bifurcate ROOT[RESULT, JUNK]; |
| ~ATH(TEMP) { |
| bifurcate TEMP[TEMP, JUNK]; |
| bifurcate RESULT[RESULT, JUNK]; |
| } EXECUTE (NULL); |
| # RESULT now contains 255 - 249 = 5 + 1 = 6 |
| THIS.DIE(); |
|__________________________________________________________|
And I think that's where I'll leave this off, as from here
writing more complex programs with numbers in ~ATH gets more and
more verbose, but not much more difficult conceptually. I will
admit that most of my ~ATH code is leaving a large chunk of the
language's potential untouched, as it all is using this numeric
structure that ignores most of the binary tree. Maybe one day
I'll get around to exploring the possibilities a litle further.
If you actually read all this, I hope you enjoyed hearing about
my silly project I put WAY too much thought into. I also really
do recommend you check out the links section of my gopherhole if
you're curious about this stuff. Overall, I think esolangs are a
really fun way to give yourself a challenging framework within
which to attempt to solve what are normally simple problems, and
I definitely think I'll try to create an original language of my
own in the future. Thank you for reading!