Trilangle is an interpreted 2-D esolang that I made in February and March of this year. At some point, I had the idea to implement an ahead-of-time compiler for the language, and what resulted was a "disassembler" feature to convert it to an assembly-like syntax.
For example, when passing the cat program below with the flags
-Dn, the output is as follows:
4: GTC 5: BNG 12 7: PTC 9: POP 11: JMP 2 12: EXT
This syntax has been useful to write programs, and the disassembler has been an incredibly helpful debugging tool. It could be translated into bytecode or machine code far more easily than the original source could, though I haven't implemented that yet.
I attempted to implement this as a depth-first traversal of possible instruction pointer states with loop detection. However, it's not the cleanest code; it traverses the program twice in a way that's probably not necessary.
When I added the threading operator in version 1.1, it was an entirely new kind of instruction I had to add support for in the disassembler. The code became a bit entangled, due to the way the threading operator works.
The threading operator represents one of three operations, depending on which direction the IP is moving, and parallel "thread" execute round-robin style. The three operations are:
- "thread kill"
TKL. This can be followed by anything or nothing, similar to
EXT, as once the interpreter hits this it won't keep going.
- "thread spawn"
TSP. This takes a label as an argument, similar to
JMP. When the interpreter reaches this, the current thread splits in two; one picks up on the instruction after the
TSP, and the other starts at the given label.
- "thread join"
TJN. When one thread reaches this, it waits for a second thread to reach the same one, and then they merge their stacks and continue as a single thread.
The language lacks explicit looping/goto instructions; instead, loops are achieved by using the IP redirection instructions to get to somewhere you've already been. The threading operator is special, however: depending on which direction you're approaching it in, this may represent a "thread join" operation rather than a simple loop. Both situations mark one entry point as a jump to the other, but thread join also inserts a
TJN instruction at the location being jumped to. Once I thought I had it working well enough, I dropped this comment at the top of the file:
// FIXME: This class is held together by an uncomfortable amount of spaghetti. It seems to work, for now, but it's prone
// to error if you look at it funny. It is nontrivial to add new kinds of operations to it. The entire class may have to
// be redesigned from the ground up.
In the wise words of Tom Scott, "That was a problem for future me, and now I am future me."
I discovered a bug involving the threading operator recently. While I fixed the particular example I cited in that issue, I did so with a seemingly unnecessary check, and I would feel much better about tearing that code out and starting over.
So, with all of that out of the way:
Was my original idea (a depth-first traversal of IP states) even the correct approach? If so, how do I implement loop- and threadjoin-detection in an extensible way? If not, how should I approach it?