So, why not assembly? While some of our computers share an architecture, cross-platform audio and graphical development is unlikely to work between them.
The devlog is a collection of design notes.
Below are disjointed thoughts on the creation of a clean-slate computing stack based on the universal virtual machine strategy for digital preservation of software projects and documentation for Hundred Rabbits. The content of the devlog was presented as a talk at Handmade Seattle 2022, watch the full presentation: Weathering Software Winter.
- Part 1: A virtual machine
- Part 2: A personal computer
- Part 3: A programming language
Safe upon the solid rock the ugly houses stand:Edna St. Vincent Millay
Come and see my shining palace built upon the sand!
Acknowledgments
The realization of this project is largely due to the work of the community, with special thanks to Andy Alderwick, Sigrid Solveig Haflínudóttir, Andrew Richards, and everyone else who contributed ideas and shared their knowledge with me, without which Uxn could not have become what it is now. Nearly a year after its initial draft, a lot of uxn code has been written. In fact, this text has been written in Left, a text-editor hosted on this strangely personal computer, and this wiki was also generated by it.
A virtual machine
Emulation is the reproduction of the behavior of a computer's physical
circuitry with software. Given that an emulator can translate the actions of
one computer onto an other, the same program could be used on both.
This is called emulation.
Someone could devise the actions of a fictional computer that is not
necessarily based on existing hardware, write software for this fantastical
computer, implement an emulator for it, and use the same program on
supported systems.
This is called a virtual machine.
Over the years, I wrote software over various frameworks for a multitude of peripherals. The vast majority is now defunct after vanishing with the platform they were targetting or by falling behind on the requirements of their ever-changing toolchains. Bitrot is the inability to access digital data because hardware and software no longer exist to read its format. Perhaps it's just a matter of time until people build emulators to make these projects usable again, otherwise these projects were never truly mine, and my learning of these languages only ever belonged to the platforms.
I. An Adequate Number Of Bits
During my research into portability, I kept thinking about how frictionless it is to play classic console games today. Pulling on that thread led me to projects designed explicitly for virtual machines, such as Another World which is equally easy to play today due to its targeting of a portable virtual machine, instead of any ever-changing physical hardware.
For a time, I thought I ought to be building software for the NES to ensure their survival over the influx of disposable modern platforms — So, I did. Unfortunately, most of the software that I care to write and use require slightly more than an 8-button controller.
So, why not the Commodore 64? Having implemented a NES emulator I found that, in comparison, implementing a c64 emulator is a monumental project.
II. Tarpits & Houses Of Cards
If the focus of this experiment is to ensure the support of a piece of code by writing emulation software for each new platform, the specifications should be painless to implement. Let's use the time one would need to write a passable emulator as a limit in complexity for this system. Could a computer science student implement an emulation of the 6502 instructions in an afternoon? Could that design be simplified, changed in some way to make it more approachable for would-be implementers?
- Subleq is a One-Instruction architecture which takes at most 15 minutes to implement. But what it does away in emulation complexity, it offloads onto the toolchain needed to make intelligible programs.
- Smalltalk is a complete computing environment and virtual machine, that was said to take about a year for one person to implement.
So, let us also set a limit to the complexity of the toolchain, since it would be an equally Herculean task to build an emulator and assembler for a machine with thousands of instructions; or a single instruction machine building abstract logic from thousands of primitive parts.
The complexity of our virtual machine runtime and toolchain implementation cannot exceed that which can be done within a weekend.
So, why not the Chifir? Because of its very incomplete specification, unspecified behaviors, and lack of testing software, it is doubtful that general purpose computing is possible on such a system.
III. Things Betwixt
In 1964, a computer scientist proposed an abstract machine with 10 instructions and 4 stacks. The superficially documented implementation specifies a list processing system capable or hosting functional languages. The system was later expanded with arithmetic and IO operations, but rests on an intricate and inefficient garbage collected system.
In 1977, a programmer wrote a small virtual machine with 36 instructions, 16 registers and 4096 bytes of memory. It had no mouse device, its controller is 16 keys organized in a square, the screen is barely capable of displaying readable text, but I was able to write an emulator and an assembler for it, in an weekend.
In the early 1980s , when computer access was still not yet widespread, a paper computer was designed, consisting of a piece of paper with 21 lines of code and eight registers. The instruction set of five commands(inc, dec, jmp, isz, stp) is small but Turing complete, meaning that it can approximately simulate the computational aspects of any other real-world general-purpose computer, and is therefore enough to represent all mathematical functions.
So, why not Pico-8? The Pico-8 comparison comes from people conflating Uxn with Varvara. A better comparison would be Uxn and the Lua VM, that Pico-8 runs on, which isn't intended to be targeted directly and its opcodes are an internal implementation detail that regularly breaks between versions. On the other hand, Uxn is a VM focused on long term stability, reimplantation, and portability.
Somewhere along this voyage into finding a suitable host for my programs, I began thinking about electronic waste, and I couldn't justify surrounding myself with yet more electronics. This dream platform would therefore be designed to be emulated, its complexity would be designed around the complexity of software and not that of hardware.
IV. Back & Forth
The balancing act of virtual machine instructions, assembler, emulator and the resulting capabilities of its language eventually brought me back to stack machines.
Concatenative languages consist of breaking a program into a list of words, and to interpret each word, words are often combinations of other words, combined to create more complex words. Brackets and parentheses are unnecessary: the program merely performs calculations in the order that is required, letting the automatic stack store intermediate results on the fly for later use. Likewise, there is no requirement for precedence rules.
operation | 3 | 10 | 5 | + | * |
---|---|---|---|---|---|
stack | 3 | 10 | 5 | 15 | 45 |
3 | 10 | 3 | |||
3 |
In Forth, memory is made of blocks of cells, which
are typically 16-bits in length, meaning that each
piece of data is a number from 0 to 65535. For this specific imaginary system,
I wanted the memory to consist of cells of 8-bit, or numbers from 0 to 255. For
example, the 12 / (34 - 12)
sequence is equivalent to the 6
bytes:
uxntal | # 12 34 OVR SUB DIV binary | a0 12 34 07 19 1b
Using stack-machine operations as primitives, along with enough arithmetic and bitwise functions as to not require to abstract computation to a higher level language, in order words to keep the assembly programming pleasant, we find the resulting virtual machine and 32 opcodes. The result is an expressive and extendable virtual machine that can be implemented in a weekend exposing a user programmable assembly running at a reasonable speed.
20 a b (c8)P+=[P] &20 x8 <> x16 40 a b c P+=[P] &40 x <> | x 60 a b c : P P+=[P] 80 a b c [P] &80 a b c @ 00 . 08 a b==c 10 a b [c8] 18 a b+c 01 a b c+1 09 a b!=c 11 a [c8]=b 19 a b-c 02 a b 0a a b>c 12 a b [P+c8] 1a a b*c 03 a c 0b a b<c 13 a [P+c8]=b 1b a b/c 04 a c b 0c a b P+=c 14 a b [c16] 1c a b&c 05 b c a 0d a (b8)P+=c 15 a [c16]=b 1d a b|c 06 a b c c 0e a b : P P+=c 16 a b [D+c8] 1e a b^c 07 a b c b 0f a b : c 17 a [D+c8]=b 1f a b>>c8l<<c8h
An implementation of the runtime, capable of running the self-hosted assembler is about 150 lines of C. Uxn cannot error and has no unspecified behaviors. Its documentation encourages re-implementation instead of adoption of a specific implementation. It operates on bytes as to remain portable on small systems, abstracting I/O entirely to the host system via dedicated opcodes.
A programming language
It is fair to assume that programming with so few primitives, will make it difficult for software tools, to infer meaning from a program's source files. But we can still convey a lot of details about the intent of various data structures by encoding information in the few tools at our disposal.
I. Structural Editing
Let's consider the following uxn program, and associated symbols file, created with Drifblim:
hello.rom a001 1560 0001 0040 0005 9480 1817 2194 20ff f722 6c48 656c 6c6f 2057 6f72 6c64 21 hello.rom.sym 0018 write 0100 ( -> ) 0100 on-reset 0107 ( str* -: ) 0107 <print> 010a ( -- ) 010a <print>/while 010a ( send ) 010f <print>/ 010e ( loop ) 0115 hello-txt
It is possible to recreate a textual source file by walking through the program data, drawing from the symbols file, as: first the labels, second the sublabels, lastly the comments, and create the following valid program:
|0018 @write |0100 @on-reset ( -> ) ;hello-txt <print> BRK @<print> ( str* -: ) !& &while ( -- ) ( send ) LDAk .write DEO ( loop ) INC2 & LDAk ?&while POP2 JMP2r @hello-txt 48 65 6c 6c 6f 20 57 6f 72 6c 64
While this text stream can be reassembled into the program from which it originates, it will first need to be reformatted to be readable as a programming artifact.
|0018 @write |0100 @on-reset ( -> ) ;hello-txt <print> BRK @<print> ( str* -: ) !& &while ( -- ) ( send ) LDAk .write DEO ( loop ) INC2 & LDAk ?&while POP2 JMP2r @hello-txt 48 65 6c 6c 6f 20 57 6f 72 6c 64
By breaking on absolute padding, label tokens, non-defining comments, and tabbing the content of sublabels, we can improve readability further and can already return to something like what the original source file might have looked like:
|0018 @write |0100 @on-reset ( -> ) ;hello-txt <print> BRK @<print> ( str* -: ) !& &while ( -- ) ( send ) LDAk .write DEO ( loop ) INC2 & LDAk ?&while POP2 JMP2r @hello-txt "Hello 20 "World! 00
Finally, we can ensure that lines terminate on emiting
opcodes(STZ/STR/STA/DEO) and some immediate opcodes(JCI, JMI). To make explicit
that some routines are emiting, and therefore line-terminating, I chose to use
the <label>
format.
Labels marking the start of binary information use prefixes that communicate to the reassembler, how to handle the content. For example, txt for ascii characters, icn for 1-bit graphics and chr for 2-bit graphics.
II. Source Validation
Lacking data types, there is not much to go on as far as static validation, but there is still space to explore here. Type inference in Uxntal is done by checking the stack effect declarations of words, against the sum of stack changes predicted to occur based on the arity of each token in their bodies.
@add ( a* b* -: c* ) Warning: Imbalance in @add of +2 DUP2 ADD2 JMP2r
Words that do not pass the stack-checker are generating a warning, and so essentially this defines a very basic and permissive type system that nevertheless catches some invalid programs and enables compiler optimizations.
@routine ( a b -: c ) Warning: Imbalance in @routine of +1 EQUk ?&sub-routine POP2 #0a JMP2r &sub-routine ( a b -: c* ) POP2 #000b JMP2r
III. Peephole Optimization
Routines are sequences of combinators that ingest values from the stacks, some permutations of these combinators are obviously redundant and reducing these extraneous transformations can be done on source files, for example:
#12 #34 SWP POP -> NIP
Tail-call optimization happen where jumps to subroutines are followed by subroutine returns and can be replaced instead by a single jump.
@routine ( a b -: c ) SWP ;function JSR2 JMP2r -> JMP2
Going one step further, routines that would otherwise terminate in a tail-call optimization could even be relocated before their tail's location in memory and do away with the ending jump altogether. Incidentally, we leave here a comment marker to indicate to the stack-effect checker that the routine's tail will fall-through.
@routine ( a b -: c ) SWP ;function JMP2 -> ( >> ) @function ( b a -: c ) DIV JMP2r
These are just a handful of examples,
there is still many more things to explore.
The previous notes contain experiments done with self-hosted tools, from the assembly, formatting, to the validation — each tool is written it the language that it assembles, formats or validates. Each program follows the same pattern of ingesting a file path, emitting errors and warnings through the Console/error port, and emits the same file path through Console/write on success, making them composable.
A personal computer
What are computers for, anyway?
I think of a computer as a lens through which I can observe and listen to the natural world. Some mathematicians are of the opinion that the doing of mathematics is closer to discovery than invention. Mathematical beauty is the aesthetic pleasure derived from the abstractness, or orderliness of mathematics. Similarly to how one might enjoy spending time studying venation patterns, we can imagine a device to explore procedural music and parametric illustration.
A romantic vision of the luddite, potentially emancipating, power of computing was promptly smothered by military drones, surveillance hardware and the advertisement machinery. Suffice to say, that nowadays, to most of my friends, computing evokes either encroaching social networks or the drudgery of data entry.
Perhaps I'm making it more difficult than necessary by using the term personal computer, and not finding a more apt word to describe what I have in mind.
I. Personal
Let's consider the whole enterprise of designing and documenting this fictional computer to be a sort of model that others could adapt to their own designs, a call to encourage individuals to conjure up their own vision of a computing machine, tailored to suit their own needs; And Varvara, a demonstration of the potential of personal computing systems, and not as a platform that should be adopted and adapted to fit broader computing needs.
The purpose of this document is not to gather would be users, but instead review potential portable computing skills that are not platform-specific, and to serve as a reminder that the key to permaculture's resilience is achieved through a diversification of methods.
I am certain that making something unique by way of customization is necessary for care, for when the unique breaks, we might mend. I often think back on how, in the movie Hackers(1995), each character had their own laptop launch sequence reflecting their aesthetics. — A far cry from today's disposable laptops, each equally adorned of proprietary services stickers.
For mending to even be envisaged, one must be able to generally understand that which needs to be mended; a venture which, thanks to the ever complexifying stack upon which modern computing is built, will have you promptly laughed out of the room. But just for the sake of this article, the required understanding of a system shall end at its bytecode bedrock.
A side-effect of working from within unique computing environments is that looking for generic solutions is often more impractical than solving the problem head on. By that, I mean by that the solution needs to be adapted to the specifics of the virtual system, and this in turn develops understanding and care.
II. Computer
The computer in this case is the sum of potential I/O communications with the underlying virtual machine. In this case, the VM can communicate with up to 16 devices at once, but has no knowledge of what a screen or a keyboard might be, these devices are what make up the computer.
Uxn is to Varvara, what the 6502 is to the Classic Nintendo.
When it came time to find out how I might want to interact with this computer, I started with a terminal, it seemed as good a place as any to start off from, by terminal I mean a console to send and receive bytes of data. The terminal communication allows me to leverage the power of the host OS to further develop the system.
As the system begins to take shape, I consider that possibly someday I may lose my sight, or the use of my hands, or I may become deaf. I wonder how I might replace one device for another in the future, and how I might adapt each device to my changing needs. I leave some blank spaces to be filled at a later time, should I need them.
Now lie in it.
Autumn is just around the corner, and when the leaves begin to fall, it will have been four years since the early sketches of a personal computing system which became Uxn. I thought it would be interesting to look back and see what has happened since.
What problem was it supposed address?
Uxn was designed explicitly to be a minimal bytecode target capable of preserving a handful of our projects, and to be flexible enough to accommodate the ones we had yet to make, in a way that would be portable and robust against bitrot.
In regard to preservation, time will tell if it was a success, but I can say that it has taken 3 years for the ISA to be frozen, and the tail end of that period only saw subtle changes that did not impact the projects made in the first two. The remaining work during that time was primarily finding, documenting and testing against the remaining undefined behaviors.
During these four years, I have seen countless developers successfully implement the runtime from scratch without my help; this is giving me hope that for as long as the documentation survives, developers can create new emulators as the hardware platforms and operating systems continue to evolve.
It is probably power hungry
Uxn, in part, was meant to help us reconcile the power-hungry tools we used with the energy we could harvest from the solar generated aboard our small boat. It is commonly understood that virtual machines are inefficient, I went ahead knowing that it might be self-defeating to target a virtual machine in which transformations are so granular that it would be hard for the host machine to optimize for.
In that way, Uxn is tolerably impractical, but it is also only one part of a larger gesture towards reducing the strain on that resource, which requires more than just putting a virtual machine at the center of it all; what was possible to do on paper, outside the browser, or natively, was thereafter done that way, but a few of our projects were still inextricably linked to portable graphical applications.
Uxn might have helped a bit, but only so much; for instance, the text-editor with which I am typing this article and use to do near every computery-thing, uses dramatically less energy than what I used previously. But I wouldn't attribute any noticeable power gain to the application running as a rom or natively. As a target, it shines most brightly in helping me to reduce the cost of iterating during software development itself, say we compare the minuscule drain of assembling a rom in comparison to rebuilding a native graphical application or reloading a webapp.
It doesn't solve a real problem
I am not convinced that computers solve any real problems either. Asking a room full of software creators "what are computers for anyways?" will get you little beyond vague suggestions of bureaucratic utility, which leads me to question whether bureaucracy is solving any real problems, and so on, ad absurdum.
But if we are to put any value in preserving digital art, music, video games and other distractions from the very real problems that basic needs demand, it might be worthwhile to address the problem head-on, and to encourage piracy at a massive scale, by duplicating digital content locally, to give data a chance to survive moving forward. Cloud platforms have all to win by making us forget that we can utilize a specific length of bytes without their saying so, and emulation is just one of many ways of exploring this issue.
The problems it is trying to solve are self-inflicted
There is a time immemorial tradition of discriminating against the various flavors of nomadic vagabonds living in tents, cars, canal boats — we are not exempt from that. On discussion forums, the economics of living on the water without a permanent address seem to elude most people, leaving them to believe that this way of life is something only the wealthy can do, that being in it is necessarily a deliberate choice untied from economic realities and that we somehow reside outside the boundary of whom deserve to be heard.
I don't think that our changing reality was what catalyzed my search for alternatives, but it might have precipitated it. At that time, browsers hadn't started force-feeding AI assistants, but it was already many years into Apple devising new ways to sabotage the repair of devices; it was becoming obvious that my choice to target the modern web browser for my little graphical toys was a vote against decentralized general purpose computing, and one for its homogenization by companies that were increasingly antagonistic to our situation.
Four years ago, had our situation been different, I don't know that I would have decided to stay with either iOS or web development. But even now, being more familiar with the landscape, I am not sure that making use of pre-existing systems would have been viable solutions for where we were trying to go.
It is too complicated, or not complicated enough
I noticed that Uxn attracts both esoteric and practical individuals. The esoteric crowd looking at the Uxn specifications are rapid to point at various alternatives for which very little software exists, and the practical ones, at the limitations and ways in which it is deficient, "the memory is too small," "the opcodes too few," "stack machines are too slow", and so on.
c 00:00.998458 haskell 00:02.870586 gforth 00:13.428614 > uxntal 00:15.861439 lua 00:16.727872 ruby 00:21.418707 python 00:32.362443 tak()
Fortunately, I have seen people come around after looking back at the typical projects that they enjoy doing, and how they can do them comfortably with less means, but there is no denying that Uxn has a specific scope in mind which does not accommodate all projects. That was never really its intended purpose, it has many times over been successful in, not acquiring users, but getting developers to explore their own visions and finding joy there.
Was it all worth it?
Is it worth it to spend hundreds of hours to save what must amount to much, much, less? It depends, if you think too much about where you're going, you might lose respect for where you are. I have friends who were complaining about a tool in 2020, they still are today; their throughput would not be much improved had they spent a years detouring to rebuild a whole tech stack. For others, who see their ideals and tools diverge, it might be encouraging to see prior explorations in that space; to those, I can only say that it is very unlikely that you will see building an environment that respects your values and idiosyncrasies as a waste of time, it might even bring you closer to others sharing in your struggles and who might become your friends.
Four years ago, I had doubts that it would work out, but time flies when you're having fun!
Special thanks to Rek, Alderwick, Sigrid, Cancel, badd10de, Bellinitte, Eiríkr, Tbsp, Wim, zzo38, Kragen, Kira, Snufkin, Virgil, SL, Sejo, gustav, soxfox, and many more.
incoming uxn about now lie in it