A tour of upcoming RFCs June 2, 2025 by spxtr
As part of our goal to become a 100-year programming language, Hare’s grammar and semantics will be frozen upon reaching version 1.0. But, before that point in time, we will make occasional breaking changes to introduce new features and fix Hare’s present shortcomings. This process allows us to become more confident in Hare’s long-term viability now, before we commit to the same design indefinitely. To make this easier on Hare programmers, we are working on a tool to assist in migrating codebases to address breaking changes.
To plan these larger changes, Hare uses a Request for Comments (RFC) process. These are discussed on the hare-rfc mailing list to reach consensus on all nontrivial changes to the language. Here, I will share a few of the accepted RFCs that will necessarily lead to breaking changes.
Handling allocation failure with nomem
Prior to Hare 0.25, the alloc
, append
, and insert
built-ins would
(usually) abort the program on allocation failure. Lorenz – who among other
roles in the project, maintains OpenBSD support – designed and
implemented a way for users to safely handle this condition
however they see fit. The result types of these operations have been adjusted to
return nomem
, a new built-in error singleton, if allocation fails. The
standard library has been updated to return this error to downstream users
anywhere it needs to allocate memory.
Slices and dynamic arrays
In Hare, slices have an internal representation which is equivalent to the following struct, with a length, capacity, and pointer to some data:
type slice = struct {
data: nullable *opaque,
length: size,
capacity: size,
};
Slices in Hare are serve two distinct purposes. On the one hand, slices are
dynamic arrays, or vectors. These slices are created with alloc
, or by
append
ing to an empty slice. They can be dynamically manipulated with
append
, insert
, and delete
, and they must be free
d when you’re done with
them. For this use-case, the capacity field is necessary.
On the other hand, slices are a “slice” of an array, or another slice. These
slices are created with Hare’s slicing operator (as in array[1..3]
). In
general, these cannot be grown or shrunk via append, insert, or delete, and
their capacity field is meaningless. Hare’s strings are a special case of this
sort of slice.
Sebastian proposed a language-level distinction between these two types of slices. The “slice of memory” sort of slices (and along with them, strings) will lose their capacity field, and using append, insert, or delete with them will raise a compile-time error. A separate vector type will be introduced to manage dynamic arrays – which you can take slices of, if necessary.
The proposal has tentative agreement, although the new syntax is not yet settled.
Mutability overhaul
Hare’s const
operator has never been properly implemented, nor has it ever had
well-defined semantics. It can be used as a type flag (as in const str
) to
indicate that a variable is not to be modified. It can also be used in place of
let
(as in const x = 5
) to indicate the same. Or at least that’s the idea,
but as of writing, const
is inconsistently implemented in Hare and often does
not enforce narrower semantics than non-const types.
Sebastian proposed an overhaul of the type system based on the principle
that mutability is a property of objects, rather than types. Under this
proposal, slice and pointer data become immutable by default and require an
explicit mut
keyword to be mutable, among other changes. For example,
export fn memcpy(dest: *mut opaque, src: *opaque, n: size) void;
The exact syntax and semantics are not yet set in stone, but the basic idea is approved. In Sebastian’s words, “this will break nearly every existing Hare codebase” – but we hope that our work on automated migration tools will ease the upgrade process somewhat.
Linear types
The last change on today’s agenda has not yet become an RFC, but is the subject of considerable interest among those working on Hare.
Hare already features a number of safety guarantees over C. It has “spatial” memory safety, which means that it prevents mistakes like buffer overflows and null pointer dereferences. However, it does not have “temporal” memory safety, meaning it will not prevent mistakes like use-after-free.
We suspect that Hare’s preferred solution to this problem could be linear
types. In a linear type system, certain objects can be marked as “linear”,
which requires these objects must be used exactly once. For instance, an opened
file descriptor is a linear int
, and it must be used exactly once. To use the
file more than once, you’d pass it to a function like write
, and write
would
return a “new” file descriptor back to you – at least, “new” so far as the type
system is concerned. A function like close
would “use” your file descriptor
without returning a new one, satisfying the requirement to use it exactly
once – and preventing both use-after-close and file descriptor leaks at the
same time.
If that’s a little confusing, I recommend reading the Austral language’s description of how they implemented linear types for a more in-depth explanation.
It is relatively straightforward to write down rules for a linear type checker in Hare based on threading values through functions. The problem is that programming within this framework is overly restrictive. We have not yet figured out the right way to relax the rules to allow for convenient usage of linear types while maintaining safety and simplicity. So, there is not yet an RFC to approve, and the feature may never land in Hare. It would be pretty neat if it did though, wouldn’t it?