Writing a Programming Language for Fun

15 Jul 2024

For a little under a year I’ve been working on a programming language project. It’s a high level language that compiles to WebAssembly, designed to small games or scripts. Here I’ll introduce the language, explain my design decisions thus far, and complain about naming things.

An Example

Before I get into the weeds, I figured I should show off what a snippet of the language looks like:

import self.tilemap.Tilemap;
import self.tilemap.Tile;
import self.character_controller;
import self.character_controller.Input;

const TILE_SIZE = 32;

struct State {
    player: Player,
    map: Tilemap,
    input: Input,

    fn tick(self: unique State) {
        // Update
        character_controller.update_player(unique self.player, unique self.input, ref self.map);

        // Draw
        sdl.set_color(255, 255, 255, 255);
        sdl.clear();

        borrow player = ref self.player;
        sdl.set_color(255, 0, 0, 255);
        sdl.fill_rect(player.pos.x, player.pos.y, TILE_SIZE, TILE_SIZE);

        borrow tilemap = ref self.map;
        sdl.set_color(0, 0, 0, 255);
        let tile_x = 0;
        while tile_x < tilemap.width {
            let tile_y = 0;
            while tile_y < tilemap.height {
                let idx = tile_x * tilemap.height + tile_y;
                case tilemap.tiles[idx] {
                    Empty => {}
                    Block => {
                        sdl.fill_rect(tile_x * TILE_SIZE, tile_y * TILE_SIZE, TILE_SIZE, TILE_SIZE);
                    }
                }
                tile_y += 1;
            }
            tile_x += 1;
        }

        sdl.present();
    }
}

With this little example in mind, why have I created this programming language?

(Writing a Programming Language) for Fun

Principally I started this project to enjoy the process and craft. Compilers are some of the most software-ass-software I’ve ever written: take as input a structured formal language, and output another structured formal language. It really satisfies the puzzle-solver in me.

Given the project’s hobby origins, it has hobby goals. I am willing to make compromises on the language itself, the compiler, and the output all in service of having a good time. If I have to trade off a little expressiveness in the language to save a little complexity in the compiler, I’ll often take that trade. With that in mind, what am I compiling?

Writing (a Programming Language for Fun)

When I program for fun, it’s almost always in Rust. In many ways Rust is my dream language: an imperative programming model, a broad ecosystem, a WASM backend, a good safety story, and great tooling like Cargo. For games and little scripts, I find Rust less satisfying because of high compilation times and inconvenient zero-cost abstractions (e.g. having to repeatedly clone strings or Rcs).

My language is aimed at the niche of smaller programs which don’t need the full weight of Rust’s performance or expressive power. Its goals are to be easy to use, enable quick iteration, prevent silly mistakes, deploy to both web and desktop, and run efficiently enough to make games. Implementation concerns are allowed to override design concerns, but ideally they won’t have to. In making early design decisions, I took inspiration from Rust, TypeScript, my half-baked understanding of Go, Notes on a smaller Rust, The Rust I Wanted Had No Future, Implementing interactive languages, and Austral.

My first and easiest decision was types and type inference: all expressions have static types, and type information flows only in one direction. For the kind of programs I want to write, a full Hindley-Milner-style system seems like overkill. Instead each expression has a type entirely devoid of its context, and type information flows from the right hand side of declarations to the left. This is fundamentally similar to most imperative languages, especially ones that bolt on an auto or var keyword later in their design. We’ll see if I come to regret this decision when I get into more complex collections, but I think it’s an appropriate tradeoff.

Struct-style product types were an easy choice, but it took me a little while to decide on my sum type. Originally in the interest of a simple language I considered something along the lines of variant records, as laid out by Bob Nystrom . I never implemented this idea completely, instead writing something that was an unholy combination of a variant record and a Rust enum. You could access each variant like a field, but it returned a nullable result. Once I was actually trying to write programs, I realized I wanted a proper sum type and implemented basic pattern-matching over my enums. That brings us to structs and enums, with limited pattern matching over the latter.

Very early on I decided that this language wouldn’t permit mutable aliasing. Aside from the correctness and performance benefits, mandating that mutable references are exclusive makes code easier to read. I considered trying to learn about mutable value semantics because of interest in Hylo and Zest, but they were out of my comfort zone[1]. I’ve opted for the more familiar Rust-style move semantics. Types may either be cheaply copied or are treated as affine[2], but not both.

Unlike Rust, I have decided that borrows are not a first-class type. They cannot be stored in user types, added to collections, returned from functions, or assigned to variables. Allowing borrows to potentially escape the scope they’re created in makes borrow-checking much harder. As a result I don’t plan to need lifetime annotations on borrows, which saves quite a bit of complexity and learning curve. Each borrow is limited entirely to its original lexical scope, and borrows may not be reassigned once created. As a result borrow-checking is a few hundred lines of code, and should be much less daunting to newcomers.

The backend for the language is WASM[3]. It lets me target the browser or desktop with relative ease, and is totally cross-platform. So far it seems easy to emit and fast enough for my needs. My biggest frustration is the distinction between the program stack and linear memory; I understand the reason (security, ability to optimize, etc) but it does make reference types fairly annoying.

Speed of execution has mostly worked out to my liking as a result of earlier decisions. When the types are known at compile time the memory layouts are as well, which makes emitted code pretty simple. I haven’t added any optimization yet; I think there should be some significant low-hanging fruit once I decide to start.

Iteration speed also has just sort of worked out. The compiler is written in Rust and makes heavy use of rayon: files are parsed and typechecked in parallel, and then functions are desugared, borrow-checked, and lowered to the “linear IR” in parallel. Code isn’t emitted in parallel, but it could be without much of a change. Compiling to WASM also means that I can implement hot-reloading quite easily. All that I need to do is create a new WASM instance and copy the memory from one instance to the other. I’ve already had fun writing a very tiny platformer character controller without having to close the running game.

Naming the Damn Thing

I’ve been working on this project on-and-off for about eight months. It’s now far enough along to write small toys, and I’ve begun writing a game. What I still don’t have is a name! The first name I picked was “brick”, a non-trademark-infringing nod to a toy that you use to build toys. After a while I discovered there’s already a hobby PL project with that name, albeit one that seems to have been abandoned six years ago. I’d like to avoid a name collision if possible, but have found no other name candidate that called strongly to me. I’ve considered:

Ideally a name would make some reference to the just-for-fun nature of the language. Secondarily I considered something that would obviously scan as a “smaller Rust”, but none of my ideas (“tarnish”, “oxidize”, etc) appealed to me. At this point I’m weighing going back to “brick”, but I’d really like to avoid the name collision. If you have any suggestions for names, please feel free to send them my way!

In future posts I plan to elaborate on specifics like the design of the compiler, linking the runtime, the language server, and features like coroutines or interfaces (both currently half-working).


  1. I could see myself revisiting this decision at some point in my batch at the Recurse Center, but currently my plate is pretty full without mutable value semantics. ↩︎

  2. I wish the language around substructural type systems was less opaque. Please check out Without Boats’ post “Ownership”. It is the clearest explanation of “affine” and “linear” types I have read. ↩︎

  3. There’s also a tree-walking interpreter that runs on the lower-level IR directly. I wrote this early in development so I could build out the language in vertical slices. It’s slow and not intended for actual use, and has mostly served its purpose now that I emit WASM. The next time it slows down development I’ll probably delete it. ↩︎