Meow5: The Conclusion

What is Meow5 and how does it work?
Meow5 is a programming language experiment written in assembly targeting i386 (32-bit) under Linux, and the second "season" of Assembly Nights.
It is also a "concatenative" language in two senses:
-
In the usual sense, it means that programs are composed by stringing functions sequentially and then passing values from one to the next (like Forth, Joy, Factor, or Unix pipes).
-
In an unusual sense, Meow5 composes functions by concatenating machine code.
Machine code concatenation is also known as "inlining". Instead of calling a function from a "call site" and returning after the function is done, the raw CPU instructions for that function are written (you could say "expanded") at the call site. When you run the program, the final effect is the same.
It turns out that as a side effect, this makes Meow5 an interactive just-in-time compiler. Meow5 programs can be run in the interpreter interactively, or they can be written to disk as tiny stand-alone Linux ELF executables. We’ll dive into this in a moment.
Concatenative data flow
Let’s take a quick tour. Here we have the "hello world" of concatenative programming:
"Meow!\n" print Meow!
This tiny example shows the concatenative data flow - the "Meow!\n"
string comes before print
. The function expects to find the string to print already on "the stack", waiting for it.
If we don’t have a string for print
, we get a segmentation fault, which is the computer equivalent of a hissing cat:
print Segmentation fault
What happened here is a stack underflow. We tried to access a value from an empty stack. Let’s get a disclaimer out of the way: I programmed almost no safety checks into Meow5. Almost any mistake will result in a seg fault!
You can also do the usual RPN-style mathematical operations and stack manipulations. In these examples, ps
means "print stack":
3 3 + ps 6 5 + ps 11 4 4 + * ps 88 dup dup ps 88 88 88 pop ps 88 88 pop ps 88
This is often called stack-based programming and is the hallmark of languages such as Forth and PostScript.
Concatenative function composition
Concatenative languages can act a lot like human speech. In fact, Forth calls its functions "words" and stores them in a "dictionary".
Functions in Meow5 are defined with def
, which is short for 'definition'. It’s fun to make silly examples:
def pour 1 + "Poured a cup.\n" print ; def drink 1 - "Drank a cup.\n" print ; def set-table "Table set.\n" print 0 ; def look dup "There are $ cup(s) of tea.\n" print$ ; set-table look Table set. There are 0 cup(s) of tea. pour pour drink pour look Poured a cup. Poured a cup. Drank a cup. Poured a cup. There are 2 cup(s) of tea. def pour-two-drink-one pour pour drink ; pour-two-drink-one look Poured a cup. Poured a cup. Drank a cup. There are 3 cup(s) of tea. drink drink drink look Drank a cup. Drank a cup. Drank a cup. There are 0 cup(s) of tea.
The pour-two-drink-one
function is composed of other
functions by naming them in order. This composition can be
used as a larger composition and so on. This is somewhat
true of most languages with functions, but a concatenative
language uses this as its primary (or even sole) mechanism
for building up programs.
A brief aside: Negative tea
Are you wondering what would happen if we drank one more cup after reaching zero? Let’s find out!
drink look Drank a cup. There are 4294967295 cup(s) of tea.
That’s no accident. Meow5 can help us get to the bottom of this by displaying the value in hexadecimal:
hex look There are ffffffff cup(s) of tea.
Ah, that’s better. This is what you get when you subtract 1 from 0 in a 32-bit two’s complement computer. This value either means 4,294,967,295 (if we treat it as an unsigned integer), or -1 (if we treat it as a signed integer).
So we either have a lot of tea or we owe the universe a cup tea, depending on how we consider our little tea-drinking model.
Meow5 can also display numbers in binary, which can be a fun visual aid for two’s complement tea drinking:
set-table look pour look pour bin look drink look drink look drink look drink look drink look drink look Table set. There are 0 cup(s) of tea. Poured a cup. There are 1 cup(s) of tea. Poured a cup. There are 10 cup(s) of tea. Drank a cup. There are 1 cup(s) of tea. Drank a cup. There are 0 cup(s) of tea. Drank a cup. There are 11111111111111111111111111111111 cup(s) of tea. Drank a cup. There are 11111111111111111111111111111110 cup(s) of tea. Drank a cup. There are 11111111111111111111111111111101 cup(s) of tea. Drank a cup. There are 11111111111111111111111111111100 cup(s) of tea.
This is just playing around, but I hope you get a sense of the natural language feel you can get from a concatenative language. The long string of commands at the beginning is uninterrupted with anything that resembles "computer syntax" and is also devoid of variable names or other parameter-passing mechanisms. Each "word" in the statement simply operates on the value left by the previous word.
I also like this example because it highlights the almost absurd juxtaposition between the high-level wordplay of the tea scenario with the low-level behavior of bits and bytes in the CPU!
Concatenative code compilation: Inline all the things!
Now let’s dive into the part that makes Meow5 unique: machine code concatenation as the mechanism of function composition.
Every def
in Meow5 is a sequence of i386 machine code.
We can see this machine code with the inspect
def.
Let’s look at really simple instruction, pop
, which
literally translates into a single byte i386 machine code instruction:
inspect pop pop: 1 bytes IMMEDIATE COMPILE 58 1 2 3 4 ps 1 2 3 4 pop pop ps 1 2
Or here’s a 3-byte one, dup
, which duplicates the value on the top of the stack:
inspect dup dup: 3 bytes IMMEDIATE COMPILE 58 50 50 1 2 ps 1 2 dup dup dup ps 1 2 2 2 2
Sharp eyes will note that dup
begins with the machine code of pop
.
The Meow5 magic happens when we compose them into a new def and the machine code is concatenated inline:
def pppd pop pop pop dup ; inspect pppd pppd: 6 bytes IMMEDIATE COMPILE 58 58 58 58 50 50 1 2 3 4 ps 1 2 3 4 pppd ps 1 1
As we can see, this new pppd
def is exactly what we asked for: three copies of pop
followed by one copy of dup
.
The magic is that there is no magic. Meow5 is just copying and pasting.
This brings us to Meow5’s namesake program, five copies of a "meow" printing statement:
def meow "Meow!\n" print ; meow Meow! def meow5 meow meow meow meow meow ; meow5 Meow! Meow! Meow! Meow! Meow!
These string printing defs are considerably larger than the dup
and pop
examples:
inspect meow meow: 45 bytes IMMEDIATE COMPILE e8 7 0 0 0 4d 65 6f 77 21 a 0 58 50 50 58 b9 0 0 0 0 80 3c 8 0 74 3 41 eb f7 51 5a 59 bb 1 0 0 0 b8 4 0 0 0 cd 80 inspect meow5 meow5: 225 bytes IMMEDIATE COMPILE e8 7 0 0 0 4d 65 6f 77 21 a 0 58 50 50 58 b9 0 0 0 0 80 3c 8 0 74 3 41 eb f7 51 5a 59 bb 1 0 0 0 b8 4 0 0 0 cd 80 e8 7 0 0 0 4d 65 6f 77 21 a 0 58 50 50 58 b9 0 0 0 0 80 3c 8 0 74 3 41 eb f7 51 5a 59 bb 1 0 0 0 b8 4 0 0 0 cd 80 e8 7 0 0 0 4d 65 6f 77 21 a 0 58 50 50 58 b9 0 0 0 0 80 3c 8 0 74 3 41 eb f7 51 5a 59 bb 1 0 0 0 b8 4 0 0 0 cd 80 e8 7 0 0 0 4d 65 6f 77 21 a 0 58 50 50 58 b9 0 0 0 0 80 3c 8 0 74 3 41 eb f7 51 5a 59 bb 1 0 0 0 b8 4 0 0 0 cd 80 e8 7 0 0 0 4d 65 6f 77 21 a 0 58 50 50 58 b9 0 0 0 0 80 3c 8 0 74 3 41 eb f7 51 5a 59 bb 1 0 0 0 b8 4 0 0 0 cd 80
Which gets at the heart of the thing: the meow5
def is, indeed exactly five times as large as meow
. And if we use meow5
multiple times inside another def, like this:
def excited-cat meow5 "\n" print meow5 ;
Then we’ll be multiplying the duplication again and on and on.
The question is: at what point is the duplication harmful? And what does a modern out-of-order executing, predictive, pipelined CPU with megabytes of on-die cache think of code written this way? I suspect the answer varies as circumstances change. I’ve found that I’m not all that interested in benchmarking it myself. I just wanted to see it work.
Meowing elves
As I alluded above, one of the really cool things about this concatenated machine code approach is that every def is a completely self-contained copy of all of the other defs it uses. You can think of it as "inlining" on the small scale and "static linking" on the large scale (except it’s not linking, it’s more inlining).
Meow5 comes with a command called elf
which writes out a def as an ELF executable. (And, beware, it will do this whether or not the def is actually a viable stand-alone program.)
Let’s try it! I’ll interactively "compile" a new stand-alone hello world program, test it out, and then write it to disk:
$ ./meow5 def meow "Meow!\n" print ; meow Meow! elf meow Wrote to "meow". exit
Now let’s try out the written executable:
$ ./meow Meow! Segmentation fault $
Well, it printed but then, oh dear! What happened?
Well, here’s something that is usually hidden from users of "normal"
programming languages: the CPU actually has no idea where our program
ends! It got to the end of our print
instruction and…just kept
executing whatever nonsense happened to be in memory after that.
To stop the program cleanly, we need to tell the operating system to exit our program. How do we do this? Okay, this is really fun.
You see how I exited the Meow5 interpreter with the exit
def above?
We can use that same exit
def in our program to quit it cleanly. To
indicate that everything was okay, we also explicitly exit with the status
0
by putting a 0
on the stack for exit
to pass to Linux with the
exit system call:
$ ./meow5 def meow "Meow!\n" print 0 exit ; elf meow Wrote to "meow".
You’ll notice I did not test the meow
def in the interpreter this time
before I wrote the ELF file. Can you guess why? :-)
Let’s test this executable:
$ ./meow Meow! $
Yay, no segmentation fault this time! This is a well behaved hello world.
How big is this thing, anyway?
$ ls -l meow -rwxr-xr-x 1 dave users 142 Nov 19 10:06 meow
It’s 142 bytes. Not too shabby. Especially since the ELF header is 84 of those bytes (52 bytes for the main header + 32 bytes for the program header). Which leaves 58 bytes for our program itself.
Does that add up? We can find out!
$ ./meow5 def meow "Meow!\n" print 0 exit ; inspect meow meow: 58 bytes IMMEDIATE COMPILE e8 7 0 0 0 4d 65 6f 77 21 a 0 58 50 50 58 b9 0 0 0 0 80 3c 8 0 74 3 41 eb f7 51 5a 59 bb 1 0 0 0 b8 4 0 0 0 cd 80 68 0 0 0 0 5b b8 1 0 0 0 cd 80
Yup! The actual program is the exact contents of the meow
def. Nothing more, nothing less.
This also means the def contains the "Meow!\n"
string itself,
4d 65 6f 77 21 0a 00
(0a
is a newline and 00
is an ASCII 'NUL' to terminate the string.)
The string is inlined right there with the machine code that
prints it. I’m doing that with
this one weird trick
I re-invented, though I was not in the least surprised to learn that it’s
a common technique from the Olden Days when people did this sort of thing
all the time.
I’d originally planned to store strings and other data the "right way", in a separate memory segment as specificed in the ELF loader and I ended up writing some tools in Zig called
Mez and Zem
to assist me with this.
The mez
program became rather pretty by the time I was done
because I understand things better if I can see them.
Here’s the colorful terminal output mez
when given the meow
program we created above:
+-[ELF Header]------------------------------------------+
| 7F E L F 01 01 01 00 00 00 00 00 00 00 00 00 02 00 |
| 03 00 01 00 00 00 54 80 04 08 34 00 00 00 00 00 00 00 |
| 00 00 00 00 34 00 20 00 01 00 00 00 00 00 00 00 /-----+
+--+---------------------------------------------/
+-- Entry point address: 0x08048054
+-- Program header file offset: 0x34
+-- Program header size: 32 (0x20)
\-- Program header count: 1
|
+-- Program Header 0 at 0x34, type: PT_LOAD
+-[Program Header 0]------------------------------+
| 01 00 00 00 00 00 00 00 00 80 04 08 00 80 04 08 |
| 8E 00 00 00 8E 00 00 00 07 00 00 00 00 00 00 00 |
+--+----------------------------------------------+
+-- File data start offset: 0x0
+-- File data bytes to load: 142 (0x8e)
+-- Memory segment start addr: 0x08048000
+-- Memory segment byte size: 142 (0x8e)
+-- Memory segment flags: R+W+X (0x7)
+-- Contains entry point 0x08048054
from 8048000 to 804808e...
0x08048000 7f 45 4c 46 01 01 01 00 00 00 00 00 .ELF........
0x0804800c 00 00 00 00 02 00 03 00 01 00 00 00 ............
0x08048018 54 80 04 08 34 00 00 00 00 00 00 00 T...4.......
0x08048024 00 00 00 00 34 00 20 00 01 00 00 00 ....4.......
...Skipping to entry point...
0x08048054 e8 07 00 00 00 4d 65 6f 77 21 0a 00 .....Meow!..
0x08048060 58 50 50 58 b9 00 00 00 00 80 3c 08 XPPX......<.
0x0804806c 00 74 03 41 eb f7 51 5a 59 bb 01 00 .t.A..QZY...
0x08048078 00 00 b8 04 00 00 00 cd 80 68 00 00 .........h..
...10 more bytes to load...
You can perhaps see that the ELF header and entry point math work out the way I described them earlier.
One of the big challenges with Meow5 was that to make stand-alone executables work, all code had to be completely position independent, including the data!
I gained a much deeper understanding of the flow of machine code execution while building Meow5!
Coming from Forth
None of this project would have happened if I hadn’t done the first "season" of Assembly Nights, in which I ported the JonesForth implementation of Forth to NASM assembly.
Not only did I learn one method for implementing a Forth from JonesForth, I learned how stuff like this is build in assembly. And in the process, I got all sorts of fun ideas, including this "let’s just concatenate machine code to make programs" one.
Then, about halfway through writing Meow5, I wrote this massive article with a really long title: Forth: The programming language that writes itself: The Web Page ("Charles H. Moore and the pursuit of simplicity.")
In that article, I describe the nascent Meow5 as a "…thought experiment taken too far," which is absolutely right.
And I wrote
"Despite attempting to go my own way, it’s remarkable how many times Forth’s solution was the path of least resistance."
As a perfect example, I deviated from Forth’s string quoting. A string in Forth looks like this:
" Foo"
The reason for the extra space at the beginning is that there is a word
named "
which gathers up the rest of the input until it hits the
"
character at the end.
What this means is that there is no special syntax for strings in Forth. You
can replace the "
word with your own to change its behavior!
I decided Meow5 would have string quoting as part of the language so you can write the string above in the much more natural-looking way:
"Foo"
But as a consequence, the "
quote character became part of the language and
it can no longer be replaced by the end user. The interpreter itself checks for
the "
character.
And unlike Forth, you cannot create a def in Meow5 that starts with a quote character. You could say that this is no great loss, and I would agree. But it has literally doubled the amount of syntax in Meow5. Forth has only one syntactical element: whitespace. Meow5 has two: whitespace and the quote.
(Mind you, I think the trade-off was worth it in this case. Strings are super common and useful in all of the programming I do, so special treatment is warranted and I don’t regret the decision.)
In the giant Forth article, I also mentioned that there were several other cases where I tried to deviate from Chuck Moore’s wisdom and ended up giving up and taking his path. In each case, Moore had discovered the simplest possible way to do it. Moore’s path was that of the least resistance. If my way had been easier, I would have stuck with it, believe me!
Meow tails
Like a traditional Forth "dictionary", Meow5 stores defs as a linked list. Instead of having "heads", my defs store metadata about themselves in "tails". This has two advantages.
The first advantage is that I can write the tail metadata after I’m done writing the machine code of the def, so I know everything I need to know about it (notably the total length of said machine code).
The second advantage is that I can use the word "tail", which seems like something a cat-themed language should have.
Like Forth, you can define new definitions with the same names as old definitions, which will cause the new definition to take precedence over the old one:
def meow "Bark!\n" print ; meow Bark! def meow "Meow!\n" print ; meow Meow!
Both meow
defs still exist, which is mostly academic in Meow5, but is very important in Forth.
Meow5 has an all
def, which lists all definitions currently
in memory by traversing the tails and printing their names:
all meow meow elf get set var loop? ? ? dup pop dec inc / * - + all inspect ps printmode say print$ printnum number decimal bin oct hex radix str2num quote num2str ; return def copystr get_token eat_spaces # get_input find is_runcomp get_flags inline print strlen exit
As we can see, there are now two meow
entries listed. When we call meow
,
though, only the mostly recently defined one is used.
In Meow5, there is no reason to keep the old definition of meow
around because it
will never be accessed. But in Forth, this allows existing word definitions
to continue using whatever version of the word they happend to use when
defined.
However, both languages allow you to extend any function by calling the original and doing additional work. Here I extend a cow
def so it quotes its moo:
def cow "Moo!" print ; cow Moo! def cow "'" print cow "'" print ; cow 'Moo!'
In most Forths, the new cow
would actually "call" the old cow
.
In Meow5, the new cow
contains the old cow
.
The other thing contained in the tails are flags indicating whether the def can be called in "immediate" mode or "compile" mode or both. We’ll see an example of the difference between these two modes of execution in the next section.
A third flag, "runcomp", is deep voodoo. It indicates that the def runs while
you’re compiling a new def, rather than be compiled into the new def. If this
flag didn’t exist, it would be impossible to have a def like ;
, which
ends the compilation of the current def and writes the tail entry!
A fun example of a "runcomp" def and of the extensibility of the language is the way I implemented comments in Meow5:
# This is a comment in Meow5!
The #
character is not part of the language! It’s not syntax. It’s just a def like any other:
inspect # #: 108 bytes IMMEDIATE COMPILE RUNCOMP 8b 35 7c ...
When called, the #
def reads the incoming stream of source
code, even in compile mode, and throws away all input until it
hits a newline character!
Meow5 is an esoteric programming language
My entire goal with Meow5 revolved around proving that machine code concatenation could work. So as soon as I saw this for the first time, I was basically done:
meow5 Meow! Meow! Meow! Meow! Meow!
But part of me couldn’t be satisfied with putting all of that work into getting this thing off the ground…but leave it with no control structures!
So I came up with the "simplest thing that could possibly work."
Here’s my "if" statement. It’s a def called ?
:
def meow "Meow!\n" print ; 0 ? meow 1 ? meow Meow!
The ?
def checks for 0 (false) or non-0 (true) from the stack.
If the value was true, it runs the subsequent def. If it’s false, it doesn’t.
To make this work, there are actually two definitions of ?
.
The easy one works in "immediate" mode. That version runs in the interpreter immeditately as you type it. The example above shows this. As soon as I type 0 ? meow
and hit the Enter key, a 0
is pushed on the stack, the ?
def runs, pops the value off the stack, determines it to be "false", and skips the meow
def by eating the next token of input so meow
does not get called! If a non-zero ("true") value was on the stack instead, ?
does nothing and the meow
def runs.
The harder version is for "compile" mode. It looks identical, but the way it works is very different. Here’s an example:
def meow "Meow!\n" print ; def maybe-meow ? meow ; 0 maybe-meow 1 maybe-meow Meow!
Like the immediate mode example that came before it, the "Meow!" is only printed
when we put a non-zero (true) value on the stack. But this time, the ?
def was
part of (compiled into) another def called maybe-meow
.
To make the compile mode version of the ?
def work, the raw machine code for a conditional jump was concatenated with
the machine code of the meow
def. The size of the jump is the exact byte size of the meow
def machine code. The effect is
to skip over the meow
code when the condition is "false".
It’s actually really hard to do anything useful with this in Meow5 because I haven’t added any handy comparison operators or anything like that. But I’m sure someone can figure out how to make it execute any arbitrary program…
Anyway, once I’d figured out how to do an "if" statement, I knew loops couldn’t be too far off.
Loops: A better way to meow
Sure enough, implementing loop?
was a relatively simple matter of sandwiching a
def with two jumps. The first one is the conditional jump I used for ?
.
The second one is unconditional; it always jumps back to the conditional
so it can be tested again.
Here’s my comment in the Meow5 source:
; Compile mode "loop" keeps replaying a def so long as the ; top of the stack contains "true" (non-zero) ; [jz][...def...][jmp] ; ^ | | ^ ; +--|------B--------+ | ; +--------A---------+ ; A=len(def) + len(jmp) ; B=A + len(jz)
Makes perfect sense, right? Well, not to me! I had to work it out on paper first.
Let’s see it in action by rewriting the meow5
def with a looping version:
def meow "Meow!\n" print ; def infinimeow 1 loop? meow ; inifimeow Meow! Meow! Meow! Meow! Meow! Meow! Meow! Meow! Meow! Meow! Meow! Meow! Meow! Meow! ... ^C
Oh no! There’s no way to stop this meowing machine short of killing it with Ctrl+C.
The problem with this program is that the loop condition is always "true".
One way to fix this is have the meow
def decrement the value on the
stack so that it will eventually be 0 ("false"):
def meow "Meow!\n" print dec ; def five-meows-loop 5 loop? meow ; five-meows-loop Meow! Meow! Meow! Meow! Meow!
Awkward? You betcha! But it works and that’s good enough for me. The loop itself accounts for exactly 15 more bytes of machine code surrounding the def being looped.
If we add an exit
to the five-meows-loop
def above, we can export it as an executable ELF file
that prints five meows at the command line that weighs a mere 160 bytes
compared with the 317 byte non-looping, redundant meow meow meow meow meow
version:
$ ls -l five-meows* -rwxr-xr-x 1 dave users 317 Nov 19 09:48 five-meows -rwxr-xr-x 1 dave users 160 Nov 19 09:48 five-meows-loop
They both appear to perform identically, of course:
$ ./five-meows Meow! Meow! Meow! Meow! Meow! $ ./five-meows-loop Meow! Meow! Meow! Meow! Meow!
But the machine code is quite different!
The five concatenated copies version:
$ xxd five-meows 00000000: 7f45 4c46 0101 0100 0000 0000 0000 0000 .ELF............ 00000010: 0200 0300 0100 0000 5480 0408 3400 0000 ........T...4... 00000020: 0000 0000 0000 0000 3400 2000 0100 0000 ........4. ..... 00000030: 0000 0000 0100 0000 0000 0000 0080 0408 ................ 00000040: 0080 0408 3d01 0000 3d01 0000 0700 0000 ....=...=....... 00000050: 0000 0000 e807 0000 004d 656f 7721 0a00 .........Meow!.. 00000060: 5850 5058 b900 0000 0080 3c08 0074 0341 XPPX......<..t.A 00000070: ebf7 515a 59bb 0100 0000 b804 0000 00cd ..QZY........... 00000080: 80e8 0700 0000 4d65 6f77 210a 0058 5050 ......Meow!..XPP 00000090: 58b9 0000 0000 803c 0800 7403 41eb f751 X......<..t.A..Q 000000a0: 5a59 bb01 0000 00b8 0400 0000 cd80 e807 ZY.............. 000000b0: 0000 004d 656f 7721 0a00 5850 5058 b900 ...Meow!..XPPX.. 000000c0: 0000 0080 3c08 0074 0341 ebf7 515a 59bb ....<..t.A..QZY. 000000d0: 0100 0000 b804 0000 00cd 80e8 0700 0000 ................ 000000e0: 4d65 6f77 210a 0058 5050 58b9 0000 0000 Meow!..XPPX..... 000000f0: 803c 0800 7403 41eb f751 5a59 bb01 0000 .<..t.A..QZY.... 00000100: 00b8 0400 0000 cd80 e807 0000 004d 656f .............Meo 00000110: 7721 0a00 5850 5058 b900 0000 0080 3c08 w!..XPPX......<. 00000120: 0074 0341 ebf7 515a 59bb 0100 0000 b804 .t.A..QZY....... 00000130: 0000 00cd 805b b801 0000 00cd 80 .....[.......
Versus the loop version:
$ xxd five-meows-loop 00000000: 7f45 4c46 0101 0100 0000 0000 0000 0000 .ELF............ 00000010: 0200 0300 0100 0000 5480 0408 3400 0000 ........T...4... 00000020: 0000 0000 0000 0000 3400 2000 0100 0000 ........4. ..... 00000030: 0000 0000 0100 0000 0000 0000 0080 0408 ................ 00000040: 0080 0408 a000 0000 a000 0000 0700 0000 ................ 00000050: 0000 0000 6805 0000 0058 5085 c00f 8435 ....h....XP....5 00000060: 0000 00e8 0700 0000 4d65 6f77 210a 0058 ........Meow!..X 00000070: 5050 58b9 0000 0000 803c 0800 7403 41eb PPX......<..t.A. 00000080: f751 5a59 bb01 0000 00b8 0400 0000 cd80 .QZY............ 00000090: 5949 51e9 c1ff ffff 5bb8 0100 0000 cd80 YIQ.....[.......
In some ways, the five copies version follows a more "pure" Meow5 path by using only concatenated machine code instead of conditional jumps.
Conclusion
This project took me over a year of evenings, though there were many gaps, including a 5-month one that spanned the entire summer. This is where having a dev log and shell scripts saved this project from certain abandonment. I wrote about this in Next Note and Run.sh.
For many months, I was averaging something like one or two lines of assembly code per night. Keeping it fun and focused was vital to keep me at it over that span of time.
I learned about linkers and loaders.
I discovered old-school position-independent data embedding hacks.
I discovered the advantages and disadvantages of null-terminated strings.
Late in the project, I discovered the edb debugger (github.com), which is a huge improvement over GDB for assembly program debugging, especially for programs like those exported by Meow5 with no sections or labels!
I learned a ton while I made this silly thing. Knowing it was never going to be a "real" language made it easier to experiment without fear.
You can view the source of Meow5 and all related files here:
In particular, you can "follow along" with the entire adventure by reading
the log01.txt - log14.txt
files in the repo. They capture my entire
process (including plenty of wrong turns and false starts) from start to end.
I’ve already got the next Assembly Nights "season" lined up and it’s going to be pretty ambitious. In fact, it would have seemed just about impossible to me two years ago when I began my nighttime assembly adventures.
Thanks for reading and until the next season begins, happy hacking everyone!