<=====------======------===[ 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! ruleofsix 2024/09/02 <=====------======------======------======------======------=====>