A New Programming Language
Cardinal is a new* programming language I am designing. Assuming everything goes according to plan, I aim to use it daily for most of my work at my current company (where we make software tools for customers in many fields) and also for most of my hobby game and app development.
You might notice the asterisk next to “new.” Pretty much all of Cardinal’s good ideas will be cribbed from other languages. That might seem a bit un-original, which in fact it is! I don’t think changing things is a good idea unless there are good reasons.
1. What do I need this language for?
In fact, I didn’t originally plan to make a new programming language at all. At work we are shipping code in Python and that mostly works fine; it is not fast, and the occasional version conflicts are a little frustrating too, but we have shipped stable code in it that runs for many happy customers.
In my spare time I write video games. I do these projects in a variety of languages, but the fastest I ever completed a sizeable working game was over Christmas in 2021–2022, when I wrote a text-based MUD in Wren over about two weeks. Since then Wren has remained my favorite programming language: small, fast, easy to write C bindings for, and easy to explain to new programmers.
I kept using Wren, writing JSON and Mustache libraries so I could build CGI-based server applications with it. There were some pain points however, which I will go into later in this article series.
Fixing my complaints about Wren would have broken compatibility with the language. So I decided to go ahead: and since I was breaking compatibility, why not revisit all the design choices that make a language what it is, and see if I can either justify them or change them? Thus Cardinal was born.
The next few articles will be my thought process on the design of a language I hope to use in production for many years to come; in fact I am using these articles to “think out loud” just as much as I am providing a starting point for others who are designing better languages. I am not an expert – you can read Niklaus Wirth or Bob Nystrom for much better perspectives. But I do need a language to get stuff done and none of the current alternatives are quite what I’m looking for.
It’s hard to set out topics in order here, because every decision you make in a programming language affects every other decision. Which things I discuss first will be dictated by the order in which I implement things in the interpreter, so it is almost arbitrary.
2. What am I looking for in a programming language?
Each thing I’m aiming for affects the design of the languages. Any one of these headers deserves a full article – and will probably get one, sooner or later – but here are a few of the top items and what their palpable effect on the language is.
2.1. Reasonably fast
“Fast” can have a lot of meanings in programming, but my target is very elementary: I write games, and they need to be able to consistently hit 60FPS on a Windows laptop which is where most of them will be played. It’s a bit rough-and-ready – “what laptop, exactly?” – but good enough.
John Carmack points out in his essay on inlined code that in games “worst-case performance is more important than average-case performance”. This particularly impacts garbage collectors: a stop-the-world collector will slow down the frames in which it runs, and for a game that wants to consistently hit a target speed, this is actually worse than a garbage collector that slows every frame down by a little bit. The latter is predictable; the former will make the game feel stutter-y. Conclusion: we either need to use an incremental garbage collector, or avoid having one at all.
Giving the programmer ways to avoid heap churn is also helpful. Sometimes this flies in the face of conventional wisdom: for example, immutability is usually considered a virtue, because it reduces the number of places side effects can creep into the code; however, immutable objects mean that every time you want to modify a property you have to mint a new object. This creates a lot of extra work for the garbage collector. So, the question arises: can we find ways to reduce the number of objects created that don’t lead to a substantial increase in the number of bugs?
Even features as basic as having a push(..) or append(..) method on lists
are a concession to this problem; in a truly immutable language, there would
only be tuples. By dint of reference counting and thoughtful tracking you can
probably cut down on most of the allocations in practice, but this is itself
a complicated design problem; I decided to skip it.
2.2. Easy for newcomers to pick up
Newcomers come in two kinds. There are people who have never programmed before, for whom your programming language is their first introduction to programming concepts in general – variables, loops, collections, et cetera. One key consideration here is to keep the number of concepts in the language to a minimum.
This is one of the driving forces behind Cardinal being dynamically
rather than statically typed. If a language is statically typed – what do you
do about enums? Are types nullable? How does type composition work, e.g.
List<T> or Map<K,V>? Are “pointers” a distinct concept from variables?
How many integer/float types are there? If ifs are
expressions, what is their return type? These are not bad questions to have to
ask, but choosing static typing forces you to answer them and so forces new
users to learn the answers you picked
before they can get up to speed in the language.
Accessibility to newbies is also a motivation to restrict the number of types
in the language, especially those in the core library (aka any type you don’t
have to import to use). Wren does this very well: of its core classes, only
nine are directly instantiable. Compare that to Python.
We also want to reduce the amount of syntax sugar. Minimizing the number of keywords and the number of other syntactic contructs reduces the number of things a new user has to learn. Destructuring is a prime example of this. In some languages you can write:
let {code, content} = doRequest("/api/create_item",{..});
Yes, this turns three lines of code into one. But it adds another piece of syntax for new users to learn. And this doesn’t just slow down new users; experienced programmers coming from other languages will have to grapple with how exactly your system differs from theirs. Does destructuring work on lists? What happens if the list has more elements than the pattern? What if your language distinguishes between objects and dictionaries – does destructuring work identically on both or are there separate syntaxes?
You might argue that this doesn’t matter to new users; they will pick up syntax bit by bit as they need it. The trouble is new users are usually working with existing codebases; you only start to know what’s going on once you have grokked an entire system rather than just a few lines, which requires you to know all the syntax used across that system. Also, there is a powerful psychological effect that happens when the entire significant spec for a language is small enough that you can power through it in an afternoon. It’s a big reason I picked up Wren instead of, say, Ruby.
The key point here is our programmers are Googlers; they’re not researchers. They’re typically fairly young, fresh out of school, probably learned Java, maybe learned C or C++, probably learned Python. They’re not capable of understanding a brilliant language, but we want to use them to be able to build good software. And so the language that we give them has to be easy to understand and easy to adopt. Rob Pike
That lecture is about the design of the Go programming language; but an even stronger case applies to Cardinal. At our company we’re conducting an experiment: that people who went to school for something other than CS, but got into programming anyway, can be trained to produce valuable code in a startlingly short amount of time. Of course, to make that work, we need to set them up for success. The language needs to be simple!
2.3. Reduces the number of screw-ups I make
This was the thing that originally made me want to rewrite Wren. In Wren,
variables have to be explicitly declared using the var keyword; however,
instance fields (“attributes” in other languages) are implicitly declared. The
trouble with implicit declaration is that typos can lurk in the code until a
given section is run, and when it is run the exact issue isn’t clear: Wren
instantiates fields to null, so you usually get an error which reads
method xyz(..) does not exist on Null when what the compiler should really
be saying is, “buddy, you mistyped _playerX as _palyerX.”
I make a lot of typos. If the language can catch them at compile time (even if it can’t catch all of them!) it would save me countless hours of pain.
The problem can be solved for fields pretty easily: add a field keyword (or
repurpose var) to declare fields. But we can go even further. In theory,
it isn’t possible to detect, at compile-time, whether a given method exists
on a given object, because Cardinal is dynamically typed rather than statically
typed. However, most of my “method not found” bugs have to do with typos rather
than misinterpreting which type a variable actually contains. So what if the
language detected when I call a method which is never implemented anywhere
across the codebase and warns me? This feature would be simple to implement
and have immense quality-of-life returns!
How many more such improvements can we make?
2.4. Fun to write code in!
When originally proposing his new programming language Jai, Jonathan Blow referred to the “Joy of Programming” as something that a language could deliberately optimize for. This, oddly enough, is another reason we prefer to take away many syntax sugars in favor of leaving a language with just enough features that there is one to write the solution to most problems: it is freeing! “There should be one – and preferably only one – obvious way to do it.”
This is a goal worth striving for. The joy of being able to completely understand a tool, from top to bottom; the joy of being able to learn a language in a few afternoons; the joy of being able to focus on an interesting problem rather than on which syntax to use to unpack a list. These are worth having; and I suspect that in the long term they will lead to better code quality. Let’s see if that turns out to be true!