Up to date as of May 2023.
I am working on a new
programming language called Garnet, so I wanted to write up a bit of
a progress report about what it is, where it is, and where it’s going.
This is partly to get my thoughts in order, partly to drum up interest
(if it is deserving of any), and partly to solicit feedback from anyone
who is interested. I did this at the end
of 2021, but life has been been busy so it’s been closer to 18
months since then rather than 12. So, here’s the next installment.
The core question of Garnet is “what if Rust were small?”
Basically, I think there’s a niche out there for a borrow-checked
systems language that tries to be more minimalist than Rust. If you
divide programming languages into “small” and “large”, Rust is
definitely on the large side. I don’t see anything fundamentally wrong
with this, since it means there’s lots of Useful Stuff in it, but it
also comes with downsides: Rust is difficult to learn, complicated to
implement, difficult to modify, and its (blessedly few) sharp edges tend
to be arcane and unpleasant. I also want to prioritize a lot of the
useful things that Rust doesn’t prioritize: I want to make a
tool with a stable ABI, excellent support for bare-metal programming,
and fast compile times. That way we can have programming languages that
complement each other.
So I think we need something like Rust but simpler. Now’s a good time
for it, ’cause we’re also going through a little bit of a renaissance of
low-level systems languages. Before 2015 or so if you wanted to write a
program touching raw memory your realistic choices were “C, C++ or maybe
Pascal”, but now aside from Rust there’s Zig, Hare, Odin, Austral, and
probably others I’ve never heard of. However, with the exception of
Austral, none of these have a borrow checker. I think borrow-checking is
a killer feature that should be more common, so Garnet is a small,
borrow-checked system language with a careful set of interlocking
features that hopefully will produce a useful, flexible language. I kind
of want it to be a low-level Lua. Ubiquitous, adaptable, handy and easy
to pick up and hack on.
Basically, I got type checking and inference working with generic
functions and data, figured out how to make generics work semi-nicely
without traits, and hammered out some weird edge cases of the type
system. That was basically all of 2022. I also played with compiler
backends a little, and fought a lot with my dysfunctional brain. Since
then I’ve been doing some learning and cleanup, and worked a lot on
monomorphization, which is one of those things which is not exactly hard
but a total pain in the ass. So, progress has been slow and fraught.
Apparently I am
not alone in considering monomorph annoying, which makes me feel a
little bit better. I’ll get it hammered down eventually, it’s just an
annoying blocker because some of the features of Garnet’s generic type
system can’t be expressed in Rust’s own generics and, since the compiler
emits Rust code for those moments, those features typecheck properly but
just won’t work until monomorphization is done.
However, I am now in the state where I can talk a little bit about
Garnet as if it were a real thing you can write programs in, because it
is! Not like, super interesting programs, but still nontrivial
ones! At first blush, Garnet looks like Rust mushed up with Lua, because
it is:
fn the_answer() {} =
__println(42)
end
I can’t write Hello World because we don’t have strings yet.. I
should probably add those someday but it won’t be too difficult, so for
now they’re just skipped. There’s lots of things like that I’m afraid.
Here we declare a function named the_answer()
which returns
an empty tuple, unit, {}
. It calls one compiler-builtin
function, __println()
, and returns the result of it. The
syntax is expression-based like Rust, so a block returns its last value.
Most compound statements use keywords like do ... end
or
fn ... end
or if ... then ... end
instead of
curly braces, but they work exactly the same way. Since we don’t use
curly braces for blocks, we can use them for delimiting tuples like
Erlang, so a tuple literal is written {1, 2, 3}
. Struct
literals are written {.label1 = value1, .label2 = value2}
;
it feels weird copying C but it works surprisingly well. One of the
goals for Garnet is to make tuples and structs fairly interchangeable,
so it makes me happy that they can use the same delimiters. Arrays are
written [a, b, c]
, like Rust or Python.
Most of the easy stuff is present in the language: we have
let
for declaring constant values, let mut
for
variables, primitive loop
and break
expressions, recursion, a basic collection of math and logic operators,
all that good stuff. The type system is strong with no implicit
conversion, like with Rust, because this has saved me from so many
tedious errors. There are structs, tuples, arrays, Rust-like enums aka
sum types, and C-like enums, because Rust’s lousy support for C-like
enums irks me. Sometimes you just want a sequence of integer constants.
So, this is a program you can compile and run right now:
fn fib(x I32) I32 =
if x < 2 then x
else fib(x-1) + fib(x - 2)
end
end
fn main() {} =
let val = 40
__println(fib(val))
end
The compiler is written in Rust and emits Rust code, which is a
little kooky but means that you only need one language’s toolchain
installed. There’s lots
of potential for backends to choose from but so far none of them
have stood out as the Obviously Best Solution, so this is fine to get
going with. (LLVM is slow, and I want Garnet to compile quickly.)
We have integers and booleans as our built-in types: floats,
characters and such are in the same boat as strings, which is to say not
difficult to add but I just haven’t needed to get around to it yet.
Functions are first-class types, but with no borrow checker we can’t
really do closures yet. References, borrowing and move semantics are
still in the future, and will probably be the next big hurdle. I could
add unsafe pointers but there’s not a whole lot of point yet. We have
sum types, but no pattern matching yet so they’re somewhat limited.
Arrays are written T[3]
for an array of 3
T
’s, all types compose postfix. So you can write:
fn get_center_element(T | matrix T[3][3]) T =
matrix[1][1]
end
T
is a generic parameter here, in Rust this function
signature would be written
fn center_element
. We
have slightly less punctuation than Rust; I tried to make generic type
parameters entirely inferred like OCaml but it was one of those things
that was more trouble than it was worth.
So that’s all pretty basic, on to the interesting stuff! Tuple types
are written {T1, T2, T3}
. A struct type is written
struct a: T1, b: T2, c: T