transmutrix

The First Released Jane Project

I wrote the original Ducky Sokoban way back in 2018, using LuaJIT and a small C framework for Windows I had written in Summer 2017, around my birthday. This enhanced remake is the first thing I've released to the public that's written in jane, my fledgling programming language.

It was a good candidate to port because the C backend was about ~5000 lines of code, and the game itself was mostly Lua, at only about ~3500 lines of code. I wrote it a long time ago, and my programming ability has improved a lot since then.

How it Went

Fairly well! This game is simple and well-defined, so mostly it was a matter of untangling my old Lua code and writing much simpler jane code to replace it. I do enjoy a good dynamic language, but it can lead to lots of unnecessary "OOP-ification" of things depending on your programming style.

Jane is very C-like, with some sugar and extra bells and whistles, so the code is shaped a lot differently in the rewrite: much flatter, easier to read and follow, and more assumptions are able to be safely made throughout the codebase.

Basically, I took my typical game dev workflow in C, and tried to remove points of friction. So far that has gone reasonably well, but the language is a long way from finished. Much of the time spent in development on Ducky DX was fixing compiler bugs, and while frustrating, that was kinda the point. Doing small projects like this is a good way to harden the language and push toward stability and design maturity in increments.

I have several relatively-large greenfield projects planned to use jane, and they will all benefit from little ones like Ducky DX preceding them.

Why Jane?

Jane is inspired by projects like Tiny C Compiler. It aims to provide a simple, easy to understand language that compiles fast. This is because iteration time is the biggest hurdle for me in programming.

I don't like environments where I have to wait a long time to test my code. I am a somewhat scatterbrained person and I am easily distracted. I want to know as quickly as possible what my code looks like in action, so waiting minutes for a compile affects my productivity a lot.

One solution to this is to use a lightweight framework like LÖVE, and sometimes I do use it. I'm a big Lua, MoonScript, and JS enjoyer! However, in practice, I just like C more. Or at least, I want to but C is annoying in its own right for some kinds of work. Some ideas are hard to express nicely in C, and I've written lots of bespoke text format parsers in C and don't enjoy it.

Broadly, I feel like I waste a lot of time writing and rewriting the same things, and doing the same boilerplate and setup work, over and over and over throughout my years as an indie dev.

So, jane is an attempt to fix the things that make C hard for me to use long term on large, serious projects, as well as small, fun one-offs.

Benefits of Jane

Let me open this section by saying that I have no interest in "selling you" on my language. It's just for me. Get off my lawn! But I want to share my delight.

Minimal Project Setup

No CMake! No different compiler per-platform! I usually waste a lot of time setting up C projects. Doing CMake, dragging in my dependencies that rarely change between my little games, writing lots of boilerplate compiler config.

The jane compiler and standard library are currently <5 MiB, so I just vendor the binaries and standard library files. I also vendor built artifacts of SDL3, SDL3_Mixer, SDL3_TTF etc. for my target platforms. No stupid hoopla. Many of the design choices of jane are in service of "no stupid hoopla."

Fast Compilation

This claim isn't one I make super strongly because I don't yet have a jane codebase over 50,000 lines yet. I will be better able to measure once I have a project that's nearer to 100K LOC.

That said, jane is pretty fast to compile. This is, in part, because it's not an optimizing compiler. It's single-pass without an IR, for now. I think having an IR would be nice insofar as it would simplify adding new backends I would like to have (like WASM), but my primary motivation there wouldn't be for more optimization opportunities.

If you mostly write straightforward code in the first place, and you're making little indie games and utilities, there's not a lot you need to worry about on modern hardware, and no compiler can save you from bad code. Pointer chasing in, pointer chasing out!

Typically when an indie game runs slow, it's not necessary, and the mistakes contributing most to that cost are in how high level code, or application data, has been structured at the source code level. I should know!

Most of the low hanging fruit benefits to someone like me are things like not fetching memory you already fetched, and being smart about allocating registers, both of which you can do reasonably well without a behemoth 1,000,000+ line compiler.

That's kinda the whole thing: If you eschew wide compatibility, if there is no legacy code, if there are no state actors involved in your language, and you aren't running the world on your compiler, you can afford to just not do a lot of things.

Much of the effort in mature compilers is dealing with massive enterprise and legacy codebases, security issues, and backwards compatibility. I don't give a hoot about any of that stuff. I care about typing code and seeing the code run.

Live Coding

All my C projects use live coding. I write a host application that owns all the memory, and passes a big God-struct with all the runtime context to the game code, which lives in a DLL. The DLL can be rebuilt, and the host will pick up the new version.

This is great, but even with C it can be kinda slow. For a big project like Wizwag, sometimes rebuilding the DLL takes 3-4 seconds. clang takes time to start up because it's a large, complex, mature compiler, with different goals to mine. Live coding in C is also a hack I have to repeat for each project.

Jane takes this workflow and builds it all in. Modules can be tagged for live code support, and then you can run your project with jane -watch src/foo.jane and it does the DLL dance for you. Jane will run your host application and then patch up callsites of functions from live modules when their source files change.

Combined with the relatively-fast compile turnaround, this feels very nice. A small game like Ducky DX has near-instant iteration times because I can change the code for some visual effect, hit save, and the game shows my changes in like ~50 ms.

A Spoonful of Sugar

Another thing that bites me a lot in C is needing to write bespoke serde code all the time, or otherwise use libraries that do it for me, but never feel quite as natural as the kind of blissful, intuitive string munging you get in more scripty languages.

So! A big goal in jane is to provide just enough of that flavor to keep me sane. Jane has arenas and has arena-backed strings, and there are little bits of sugar to make it feel more like working with strings in JS, Nim, or Python, but for the most part it still looks a lot like munging strings in C, just with fewer small allocations happening.

Jane also has wee bits of compile time execution implemented to facilitate the specific sorts of C macro abuse I rely on a lot: manifests of asset files known at compile time, enums populated with an X macro, etc. Some of this can be seen in episodes of The Plain Jane Gamedev Stream.

There are generic functions but this feature isn't complete or robust yet. I've only added what I need to make specific things work along the way.

The CTE offered currently is also partly there for compile-time reflection. Jane only has simple POD structs, so this isn't as complex as you might think.

The partially-done generics plus the CTE gives me the ability to make things like this work, type-safely, at compile time:

if from_strjson<Foo>(arena, src, &result) { return result; }

The idea for this comes from my favorite JSON library for a "real" language, which is jsony for Nim. The best serde code is automated serde code. Like jsony, the json library tries to be forgiving and make sane assumptions when reading a JSON file.

This library pairs well with jane's ZII philosophy: If a field is missing in the json source, it will just be zeroed out in the struct. If extra fields are there, we ignore them.

My Jane-Based Project Workflow

So, putting it all together, here's what I get currently as a dev in jane, using SDL to make little games and tools:

  1. I copy my template project which is, roughly:
template/
  jane/
    bin/             <- binaries of compiler, lsp, and packager.
    std/             <- holds .jane stdlib files.
    vendor/          <- holds prebuilt SDL artifacts.
  src/
    main.jane        <- opens an SDL window that changes colors.
    f_startup.jane   <- big live-codable function for the init.
    f_events.jane    <- for processing SDL events.
    f_update.jane    <- for the fixed-time update.
    f_draw.jane      <- for the draw.
    f_shutdown.jane  <- you get it.
    runtime.jane     <- defines the God-struct holding all state.
  tools/
    dev.sh           <- just `./jane/bin/jane -watch src/main.jane`
    pack.sh          <- uses the packager to make distributable zips
  1. I open the directory in Sublime Text.
  2. I run ./tools/dev.sh in my terminal.
  3. I work on the game. Changes appear almost immediately. My fingers gesture at an invisible shape, and it comes into view. This is the joy of programming.
  4. If I need to change the Runtime layout, I close and restart the game.
  5. When it's time to ship, I run ./tools/pack.sh and it completes in <5 seconds, giving me zips to upload to itch or wherever.

To update the compiler, stdlib, or vendored DLLs, I just copy in the updated files and commit them. Sublime Text and VS Code use the vendored lsp binary for their langauge extensions, so the LSP behavior matches the version of the compiler a given project is using.

Pain Points

Jane is still very young and buggy, and has rough edges I brush up against constantly trying to do real work in it - that's just the nature of the beast!

One big drawback of using jane is that I can't trivially emit a web version of Ducky DX. If I had used C or Nim, Zig, Odin, etc. it would have been straightforward to do an Emscripten build of the game since it relies on SDL to interact with the host platform.

This is how I've shipped some other projects to the web in the past, and it's a workflow I like! It's easy to ship a demo or free version of a game online, and offer a paid download with extra goodies in it, etc.

I would really like to ship a web version of Ducky DX. I think the game is well-suited to the web. So, what do?

Well, given that this game is only ~4500 lines of code, it wouldn't be that big of a deal to just write a version in Nim or Love2D to get a web build out the door, so that's something I'm considering.

Which feels incredibly silly! But it's much more feasible this week than adding a WASM backend to jane, so we'll see.

Long-Term Worries

It feels gratifying to finally ship something to end users created with jane. At the same time, it's strange. I look ahead at the long road of this "not invented here" life and it's lonesome, but I rarely feel at home in communities, either.

I always feel a bit like I'm a bug wearing a person skin and I'll be found out. A few people want to crawl under my warm rock with me, and that's nice, but in general your world is too bright, too loud, too confusing, too callous. I'm the earwig caressing her young, stepped on in sanctimonious disgust.

Maintaining my own language for the next 30 years could be the right route if things continue to degrade in the wider software world, and continue to be deliberately poisoned by corporate leeches.

On the other hand, I wonder if my time would be better spent venturing out to contribute more in FOSS spaces, actually being active in communities instead of lurking, and trying to make friends. Maybe it's better to be loud in the commons while they're being enclosed, than to leave early when I feel the ground shake.

One possible compromise could be trying to bring the parts of jane that have most impacted my process back to a more mainstream environment - maybe there's room to try to make live coding possible or nicer in other languages! Maybe some of the slow compilers out there could be sped up with some elbow grease.

My reaction to feeling like I don't belong in someone else's castle has always been to just make my own little castle where it's quiet. Maybe that's the best thing to do! But maybe I should be putting cozy nooks in those other castles, instead.

I don't know.