Another Modern C: The Hare Programming Language


I do enjoy writing C, it is simple, straight-forward and I have a good idea of everything that is happening and where it is stored. But damn, C is so clunky. I have been looking for modern alternative, all with their strengths and weaknesses. Please note that I don’t want to shame or talk bad about other alternatives. This is a love letter to the one I chose.

At the moment of writing, I still haven’t thought much about what I’ll say nor how much I’ll write, which means it will once again be way too long. So I will start with the most important: thank you to the devs. I know what it takes to design a language and implement a compiler. I know the insane amount of work it requires to design its standard library and maintain it. So I would like to start by a big acknowledgment of this work and by showing a bit of gratitude. I am not thinking of writing my own modern C anymore, and I think it’s beautiful.

What I love in C

C is sometimes described as a textual assembly language. Obviously it isn’t the reality, but it bears some truth. Indeed, most control-flow constructs of the language can be translated into the equivalent assembly code without much work1. But most importantly I think, the data manipulation is straight-forward. Your types exist in memory in the exact way you defined them in your code (modulo alignment, padding and all this stuff), you are in total control of how objects are allocated, when they are freed etc. Even though it is primitive, the error handling in C is also entirely under your control2.

These are naturally not always a good thing, you may prefer more comfort and more safety. But sometimes, depending on what you want to develop, being in total control of everything may be what is best for you. I’ll give you an example. I spent the month of February writing a parser for my friends' notation language SHAUN. This was supposed to be a minimal, performant and memory-efficient implementation of their language in C. There is something I’m relatively happy with: the error handling.

My error type is a 64-bit integer. Actually, it is a big union of error cases that was designed to fit into a 64-bit integer. Not only I could just decide to use an actual typedef sn_error uint64_t to define it, but the type system of C allowed me to transmute that error type into a sn_decoded_error back and forth since one was a mere integer and the other the complex union. Why would I do that? It’s simple: I have a nicely constructed error type at the price of a integer. As long as you’re not explicitly trying to decode the error (and use the macros to do it for you), you just deal with the integer. This allows you to write code like this:


sn_value val;
if (sn_load_from_string(THE_STRING, &val)) {
    // much error, oh no
}
Yep, the error value for “everything went right” is 0, which is extremely easy to check.

What is exhausting with C

As per the last example, you may already have a good idea of what the code may look like when you use this kind of error handling. It looks like Go: an endless list of if statements to manage the errors. You can’t factorize the error handling, since each function has to check for the error value, with another if etc etc.

When you allocate memory, open a file, use a resource or whatever, please don’t forget to free or close it and the end of the function, about 150 lines below. I rarely encounter errors because I forgot to free my resources, but it still forces me to remember to do it and I sometimes need to ask valgrind what it thinks of my code, to be sure I didn’t forget anything.

Type definition can be tedious and verbose at times. Having to typedef everything is not particularly a passion for me. Strings are a whole layer of difficulty with C, since it was invented long before utf-8. When you write bigger programs, you want to split the code between files. C allows that of course, but you have to bookkeep a header file (meant to be concatenated included into another source file) and the source file which defines the variables and functions that you need. It is so painful!

Sometimes I think of what a good modern C alternative could be. Something minimal and straight-forward, but with more modern features that would free me from the pains of C. I have thought of designing it myself, but you may start to know me: I can’t be concise, minimal and able to stop. All my attempts led me to design either another Rust or another Go or even a mix of both.

That’s not a rabbit, but it’s cute anyway

I did my little research. I found good candidates. Please take the time to read this: I know of Rust3, I know of Zig4, I know of Go etc. Those have one thing in common that disqualify them as C replacement: my brave Everest is absolutely unable to do anything with them: Rust and Zig take to long to compile and I can’t even bootstrap TinyGo for the dependencies filled my poor SD card.

Then I decided to try Hare. It is marketed as a systems programming language designed to be simple, stable, and robust. It has a minimal runtime (which is statically linked) and is well-suited to writing about all the things that give me fun. Let’s go!

That gemtext parser

I started that new journey writing a simple parser for texts in gemtext format. The format is simple, line-based and the parsing of each element depends entirely on the 3 first characters of the line. What could go wrong?5

I installed hare, defined some types, wrote obvious functions and then I proceeded to think precisely about how to do it. While my implementation is not exactly something I would be proud of, I must admit that it was surprisedly simple and natural to write. Hare really is a comfortable C.

First, the language uses tagged-unions profusely. This is a native feature of the language that allows you to say “well, this type foo and be either a bar or a baz” and call it a day. There is no need for boilerplate, englobing type or hand-made discriminant (the tag). Hare does it for you. For example, here is the type of the lines in my gemtext parser:


type line = (heading | link | list_item | block_quote | preformatted | str);
Each case here is a struct I had already defined6 and str is the native type for strings. Since I will want to discriminate between the different types of lines went using that type, Hare has a neat language construct to help us: the match expression. In Hare, you have switch and match. Both do the same thing but one acts on values and the other on types respectively. Hence, if you want to use a line, you can write:


match (my_line) {
case let h: heading =>
        do_stuff_with_headings(h);
case let l: link =>
        do_stuff_with_links(l);
case =>
        do_the_default_stuff();
};
I have to say that this is quite comfortable. I am finally able to write C the way I have always written OCaml, Rust or other functional language code.

The language does a good use of these tagged unions. It is how errors are handled. One can define a type as being an error type by prefixing its definition by a ! character, e.g.:


type dividedbyzero = !f64;

fn div(num: f64, den: f64) (f64 | dividedbyzero) = {
        if (den == 0) {
                return; // dividedbyzero
        } else {
                return num / den;
        };
};
Unlike Go which would require me to handle the errors at every step of my life as an adult, Hare decided to take the Rust route: using punctuation. One can use ? to return an error as-is (just like Rust’s equivalent) or even ! to tell the compiler to foda-se, não ligo com caralhos desses! and just crash here in case of error, that is particularly convenient for functions such as alloc from which there is usually no good way to recover. This enables me to write code the way I like it: tell me what you are supposed to do, we’ll handle the issues later.

That small virtual machine

These days, I’m working on virtual machines, emulators etc so it felt natural to write a small emulator for a home-made machine. The machine itself is quite simple: 32-bit, register based (R0 set to a constant 0 as in RISC-V, R1-R7, SP the stack pointer and PC the program counter, FLAGS for flags), a return stack hidden from the programmer and user and its dedicated RTSP, variable-length instructions with an 8-bit opcode. The ISA is quite small but each opcode contains more information than just an ID:

For example, a single JMP 32-bit:0xCAFE if Z which means jump at the 32-bit address0xCAFE if the condition flag is zero. Many instructions set the flags (arithmetics, load and store, etc), which allows “complex” control-flow to be expressed quite simply.

Once again, I had pleasure writing it in Hare7. The syntax of switch statements is comfortable to write and read, which is important for someone like me obsessively writing VMs, compilers and stuff like that. I did a good use of tagged unions to discriminate between register and immediate operands. Also, unlike C99 (at least), Hare has a static assert builtin which helped me test that some types have the size I wanted them to have (typically my 8-bit opcodes).

It is not perfect, but still

Naturally, Hare is far from perfect. I do face some problems from time to time. More of them are essentially due to the language and its ecosystem being young. I hope the situation will improve and I trust the developpers for that.

First comes the lack of learning resources. Granted, the language comes with a tutorial and the whole specification. This has to be acknowledged, for the textual specification serves as an available comprehensive definitive authority. There also exists a website with good examples of how to use most of the language features and how to leverage the standard library. But once you are ok with the basics8 and need guidance on more advance questions, you’re on your own.

What struck me was the sensation of being the first one to ask some questions. For example, in C, you can define bit structures such as the following:


typedef struct {
    int immediate_size_flag : 2;
    int conditiong_flag     : 2;
    int operation           : 4;
} opcode_t;
This allows to encode complex information into a single integer-like object, allowing to define values of less than a byte - even though the entire datatype will be a multiple of 8-bit long. In Hare, it seems that you can’t do that. What you have to do is define you type as being an alias for an integer and add functions to manipulate the sub-byte values:


type opcode = u8;
fn immediate_size_flag(op: opcode) u8 = op >> 6;
fn condition_flag(op: opcode) u8 = (op >> 4) & 0b11;
fn immediate_size_flag(op: opcode) u8 = op & 0b1111;
It works and I think this is how I’d compile bit-structs in C (with function inlining, etc). But it makes it quite uncomfortable to manipulate this kind of data and I think it is sad for a C alternative. I have looked about everywhere to see if at least people had ask about such a feature. But I found nothing.

I had also designed my emulator to make the instruction-execution callback call the next instruction themselves instead of relying on an interpretation loop which a switch. In C, it would have worked very well for the main compilers support tail-call optimization. If a function ends by the call to a function, it can be compiled into a jump instead of a call, which allows to use the same stack frame as the calling function instead of push another frame on top of it (i.e. you can recursively call functions indefinitely, the stack won’t overflow). But also, it allows to return from all these tail called functions with only one return instruction. Indeed, if you don’t call a function but jump into it, for the CPU, it is like you are still executing the first function, you did not push a new return address on the stack. If you return, you return from the first function - and all its children as well. However, Hare doesn’t seem to support such an optimization. Is it because of the compiler’s backend (QBE)? Is it because the optimization hadn’t been implemented yet? Is it because it is not planned to be supported at all? I don’t know. I asked on the IRC channel, received no answer and just rewrote everything with an interpretation loop.

I don’t blame anyone, it is probably quite early to implement this kind of optimization. But that’s the biggest point I’d like to highlight here: Hare is a young language, and if you want to adopt it, this is something you have to consider.

What makes it so good then?

It is not finished. It is not brilliant. Batteries are not included. But damn, it feels well designed. The language has clearly been designed by people who wanted to write C in the 2020s. The syntax in consistent, the type system is consistent, the little syntaxic sugars are welcome, the defer statement is welcome, the tagged unions are so welcome. Its module system is clear9.

You have the right amount of control

Hare is young, but it has so much potential. It is the kind of languages that enables you to write extremely simple yet powerful code. Unlike Go, it will never remove complex features from you, at the end of the day you are still the boss, there is a way for you to implement the dumbest footgun if you really want to. But just like Rust, you have to explicitly aim for your foot to circumvent the most basic checks. Let me give you an example:

In a virtual machine with a fixed 65536 (i.e. 2¹⁶) bytes of RAM, you may want to represent that memory with a simple static array as follows:


use types;

def RAM: [types::U16_MAX + 1]u8 = [0u8...];
Now, the machine uses 16-bit registers and I want to load a word from memory into a register with an instruction such as ld r1, 0xCAFE. This means that I need to read a u16 from my array of u8. I have two solutions. The first one leverages bitwise arithmetics:


fn read_word(address: u16) u16 = {
        const low = RAM[address]: u16;
        const high = (RAM[address + 1]: u16) << 8;
        return low | high;
};
It is perfectly safe as long as address does not refer to the very last byte of RAM. The second solution tells Hare to trust the programmer when they want to access RAM to read a 16-bit word, using pointer convertion:


fn read_word(address: u16) u16 = {
        const ptr: *u16 = &RAM[address]: *u16;
        return *ptr;
};
This solution has the exact same problem as the first one. The error can be handled the exact same way for both (write a guard and returning an error, leveraging tagged unions), one looks safer but uses intermediate computation. The other looks unsafe but if you think about it, as a human, you can tell it works: the address parameter is a u16, its value cannot go out of the bounds for RAM, with the exception of the last byte which we know how to manage. The convertion is safe. Just do it, don’t ask.

The compiler is simple, fast and frugal

As Hare’s creator, Drew DeVault, said, it is important that one programmer alone be able to understand the entire language and its entire compiler. In the context of permacomputing, it is of utmost importance that one could be able to maintain the compiler themself or even to rewrite it from scratch.

The fact that the official implementation uses QBE instead of LLVM for its backend reflects that. LLVM is a behemoth while QBE is a smaller hobbyist-oriented project. It allows the compiler to keep a small footprint, to be bootstrapped on smaller computers (remember my RaspberryPI 3B+?).


  1. Just a little control-flow analysis, but pinky-promise the CFG construction is the hardest part.
  2. You can totally implement an exception system if you want, it’s entirely your problem.
  3. I’m actually quite good at it
  4. And its frighteningly long list of orthogonaly compatible features, I swear the specification gave me a headache.
  5. Everything lmao, my implementation is so bugged…
  6. Most of them only contain only a text field but some like link also hold additional information.
  7. And never finish it, I guess… I just need to implement interruptions and devices and fucking test it, come on…
  8. Which should happen quickly since the language is quite simple and really does feel like a modern C.
  9. Mostly inspired by Go, I think?