StackAssembly 1.0

published on

Introduction

StackAssembly is a minimalist, stack-based, assembly-like programming language. Here's a taste:

@initialize_counter call

loop:
    @increment_counter call

    @should_stay_in_loop call
    @loop
        jump_if

255 = assert
return

initialize_counter:
    0
    return

increment_counter:
    1 +
    return

should_stay_in_loop:
    0 copy 255 <
    return

If you want to learn more about the language, the repository on GitHub is a good place to start. Or keep reading here, to find out about the StackAssembly 1.0 release.

This project started with a simple design, which I intended to be minimal enough to implement quickly, but also complete enough to write real code with.

With StackAssembly 0.1, I proved out the first part, implementing the design within a few weeks, alongside a few minor additions.

Now with StackAssembly 1.0, I'm delivering (belatedly) on the second part of that goal, having implemented a version of the classic game Snake in the language. That required a few more additions though, which I'll present in the next section.

Going against my original plan, this is the final release of StackAssembly and I consider the language complete now. I'll talk about that at the end of this post.

New and Improved Features

Here are some changes that I consider the highlights of the 1.0 release. To see a more complete list, with links to the respective pull requests, check out the changelog.

Structured Control Flow

The original design only came with unstructured jump and jump_if operators. I figured I could emulate function calls manually, by storing return addresses on the operand stack.

As I started working on Snake though, I realized quickly that I don't want to do that. So I added call and return operators, which operate an implicit call stack.

1
@push_2 call
3
return

push_2:
  2
  return

This example first pushes 1 to the stack, then jumps to the push_2 label, where it pushes 2. From there it returns to push 3, after which it exits.

Even though push_2 is used like a function here, it is just a label. We need that explicit return after the 2, or we'll reach the end of the script and exit. And without that return after the 3, which exits from the script, we'd run right through the label and push another 2.

And while forgetting to call or return was a common problem while implementing Snake, this feature still helped a lot.

I also added call_either, a conditional variant of call, but ended up not using that.

Expanded Integer Literals

All values in StackAssembly are 32-bit integers. Where that makes a difference, those are considered to be signed integers.

For the Snake game, I needed to specify pixel colors, and I wanted to use 32-bit RGBA values for that. 0xff0000ff for "red", for example.

In this case, signed or unsigned makes no difference. All that matters is the bit pattern. But the integer literals in StackAssembly 0.1 only covered signed integers, and "red" falls outside of that range.

Integer literals in StackAssembly 1.0 support all 32-bit integer values, signed or unsigned, from -2147483648 to 4294967295, in both decimal and hexadecimal.

No More Runtime Panics

StackAssembly 0.1 made it possible to trigger runtime panics in the underlying Rust code. You could do it with a very large script, or on 8-bit and 16-bit platforms by passing large operands to read, write, copy, or drop.

A huge script now triggers a panic at compile-time instead. Given that compile-time errors aren't a thing in StackAssembly, and that such a large script is unlikely in the first place, this seems like a good compromise. The other cases should trigger the appropriate effect now, instead of panicking.

Improvements to the Host API

StackAssembly scripts are sandboxed, and you need a host (Rust code that uses the StackAssembly interpreter as a library) to perform I/O for them. The API available to hosts has improved a lot.

From convenience features like accessing Memory as a slice or converting Value to and from booleans, over larger changes like adding a Script type that decouples compilation from Eval, to simplified effect handling and tools for better error reporting, there's a bit of everything.

Building Snake

I didn't make any of these improvements without cause though. They all helped me while building Snake.

A screenshot of a simple Snake game.

I'll be honest, it was a pain. Also an interesting puzzle. Painstakingly tracking the current state of the stack, writing data structures from scratch. Trying to put call and return in all the right places, but forgetting half the time.

Still, it proves that my minimalist language is complete enough for non-trivial code. And with further improvements, it would only get better from here. So what's the problem?

What's Next

I came to this endeavor from a background of working on overly ambitious projects all the time, seemingly never getting anywhere with them. Working on something simple, splitting that work into specific milestones, having something to show at each milestone; that was refreshing.

But I didn't set out on this journey just to finish something, anything. I did it to advance my personal research into the design and implementation of programming languages. And I realized, with this approach, it would take me forever to get anywhere interesting.

Planning and implementing these small releases, making sure that each one (and in fact, every pull request) was reasonably polished and well-tested, writing an article each time; all that was a lot of work. More importantly, it was too stringent, lacking for spontaneity and experimentation.

What I needed was a middle-ground: A concrete milestone to work towards, yes, but not every few weeks. Instead, something I can finish on the order of months, maybe up to a year. With polished and well-tested releases at suitable points along the way, and lots of room for prototyping in between.

And that's what I've started doing over the last few months. I'm working on a new language (code-named Monobloc). As a first milestone, I've set the goal to make music with it. For that purpose, I'm building up a kind of code-first software synthesizer (code-named Monobloc Synth).

For now, I'm developing this in a private repository. It's all very chaotic, often with many twists and turns before I arrive at the right solution to a problem. Once I have a minimal but complete prototype, I'll start extracting pieces from it, release those, and put the whole thing on better footing.

If you want to follow along on that journey, you can do so by subscribing to the posts on this website via RSS or Atom, or by following me on BlueSky.