Hare 0.25.2 released June 21, 2025 by Drew DeVault
I am pleased to announce the release of Hare 0.25.2 today. 🎉 It has been almost one year since the previous release, Hare 0.24.2, and the new release is packed with the numerous language features, standard libarary features, and countless bugfixes and small improvements which have been implemented since. I’m excited to share the highlights with you today.
Hare 0.25.2
- Release notes
- Hare 0.25.2
- Download
- compiler harec-0.25.2.tar.gz • stdlib hare-0.25.2.tar.gz
Hare 0.25.2 is compatible with version 1.2 of qbe.
New here? Hare is a systems programming language designed to be simple and robust, using a static type system, manual memory management, and a minimal runtime. You can learn about it here.
Release highlights
The headline features of Hare 0.25.2 are:
- Buffered I/O improvements
- Documentation tooling improvements
- Expanded Unix API coverage
- Integration of third-party tools into hare(1)
- Mandatory error handling in out-of-memory conditions (nomem)
- Revised and improved APIs for time/date support
- Semantic code annotations
- Tooling for updating codebases affected by breaking changes
Buffered I/O improvements
The bufio:: and memio:: modules have landed numerous improvements in this release cycle, including:
- Options to fine-tune the behavior of bufio::scanner
- A “nonblocking” mode for memio:: streams
- Managed bufio::stream modes
- os::open_buffered and os::create_buffered
- io::seek support for bufio::scanner
bufio::newscanner now accepts an optional bufio::scan_options parameter to fine-tune its behavior in the end-of-file condition, allowing the user to determine the appropriate behavior when scanning lines or tokens whose delimiter does not appear before the end of the file. Having access to these knobs is useful in particular when dealing with incomplete I/O sources, such as a socket or a pipe.
// Options which fine-tune the behavior of a [[scanner]].
type scan_options = enum uint {
DEFAULT = EOF_DISCARD,
// Upon encountering EOF, all bytes or characters between the
// final token and EOF are discarded and EOF is returned
// immediately.
//
// This option is recommended for use-cases where the user is
// scanning over a file or buffer which may contain partial
// content, and the user wishes to consume as many tokens as
// possible and assume that additional data may follow EOF
// before a new delimiter is written.
//
// This is the default behavior. Note that on Unix, text files
// are always terminated with a new line, and [[scan_line]] will
// enumerate all well-formed lines in a file with this flag --
// however, when scanning ill-formed text files which include
// text following the final line feed, this additional text will
// be discarded.
EOF_DISCARD = 0,
// Upon encountering EOF, all bytes or characters between the
// final token and EOF are treated as a token and returned to
// the caller before returning EOF.
//
// This is recommended for use-cases where EOF is effectively
// considered an additional delimiter between tokens, or where
// the remainder of the file following the final delimiter is
// meaningful.
EOF_GREEDY = 1 << 0,
};
The new “nonblocking” mode for memio is also designed to assist in dealing with incomplete I/O sources. When creating a fixed or dynamic memio stream, a new flag argument is accepted with a “nonblocking” mode:
// Flags for memio streams.
type flag = enum uint {
NONE = 0,
// A NONBLOCK memio stream returns [[errors::again]] instead of
// [[io::EOF]] on reads from the end of the buffer.
NONBLOCK = 1 << 0,
};
This allows you to create a memio stream from an incomplete buffer, and pass this stream to a parser for further processing. If the buffer is complete, the parser will process it normally, and if the data is incomplete, the parser will eventually encounter errors::again and bubble it back to the caller via error propagation, signalling that the caller should fetch more data into the buffer and try again.
We have also added a “managed” mode for buffered I/O streams in this release, which allows the user to tie the lifetimes of the buffers and source files to the buffered stream rather than manage those lifetimes independently. This is facilitated via bufio::flag:
// Flags to tune the behavior of [[bufio::stream]].
type flag = enum uint {
NONE = 0,
// If set, the underling [[io::handle]] for a [[bufio::stream]] is
// closed when [[io::close]] is called on the [[bufio::stream]] object.
MANAGED_HANDLE = 1 << 0,
// If set, the read buffer for a [[bufio::stream]] is freed when
// [[io::close]] is called on the [[bufio::stream]] object.
MANAGED_RDBUF = 1 << 1,
// If set, the write buffer for a [[bufio::stream]] is freed when
// [[io::close]] is called on the [[bufio::stream]] object.
MANAGED_WRBUF = 1 << 2,
// The [[io::handle]] and the read and write buffers are owned by the
// [[bufio::stream]] object and will be disposed of (closed or freed
// respectively) when closing the [[bufio::stream]].
MANAGED = MANAGED_HANDLE | MANAGED_RDBUF | MANAGED_WRBUF,
};
For your convenience, we have also added helper functions to os which take advantage of this feature to provide hands-off buffered I/O for the simple use-case:
// Opens a file and allocates read and/or write buffers for buffered I/O. To
// open an unbuffered [[io::file]] see [[open]].
//
// [[fs::flag::CREATE]] isn't very useful with this function, since the new
// file's mode is set to zero. For this use-case, use [[create]] instead.
fn open_buffered(
path: str,
flags: fs::flag = fs::flag::RDONLY,
) (bufio::stream | fs::error | nomem);
// Creates a new file with the given mode if it doesn't already exist and opens
// it for writing, allocating read and/or write buffers for buffered I/O. To
// open an unbuffered [[io::file]] see [[create]].
//
// Only the permission bits of the mode are used. If other bits are set, they
// are discarded.
fn create_buffered(
path: str,
mode: fs::mode,
flags: fs::flag = fs::flag::WRONLY | fs::flag::TRUNC,
) (bufio::stream | fs::error | nomem);
Finally, we have added support for io::seek to bufio::scanner objects. The next release will add support to bufio::stream as well.
Documentation tooling improvements
The haredoc tool has received some attention in this release cycle as well. haredoc will look for short, one-line summaries at the start of a module’s README, and present them on lists of modules, on the TTY and HTML outputs. haredoc also keeps track of mappings between symbols and their source code locations, and endeavours to make this available to you, by adding source links to the HTML output and through the use of the new -n and -N command line flags. The -n flag adds file paths and line numbers to the normal TTY output, and -N will look up a symbol, print its location, and exit, for quickly finding where a symbol is defined.
Expanded Unix API coverage
This release expands our coverage of the Unix/POSIX API surface, including the following features:
- io::fsync and io::fdatasync
- unix::getrlimit and unix::setrlimit
- fcntl, via os::getflags and os::setflags
Furthermore, this release ports the existing POSIX shared memory support to NetBSD.
Integration of third-party tools into hare(1)
From this release, the build driver, hare(1), can be extended by third-parties
via the hare tool subcommand. This subcommand will execute third-party tools
installed in the installation prefix’s libexecdir, e.g. /usr/libexec/hare
. The
new hare-update tool is one such example, and in the future we hope this
namespace will be expanded to include various tools that extend Hare in some
way, for use-cases such as code formatting, code generation, and so on.
Mandatory error handling in out-of-memory conditions
Users are now required to handle out-of-memory errors, unifying this class of errors with the rest of Hare’s conventional mandatory error handling requirements.
A new primitive type, nomem
, has been introduced for this purpose. nomem is an
error type and can be asserted with ! or propagated with ? accordingly. The
built-in alloc, append, and insert features now return a tagged union with the
result type and nomem, and standard library functions which allocate memory can
also now return nomem.
It is recommended that users take advantage of the hare-update tool to simplify the process of adapting their code to cope with the breaking changes. Maintainers of third-party libraries or modules written in Hare are encouraged to consider if their library interface should be adapted to include nomem as well, and to coordinate their own breaking changes with their users accordingly.
Revised and improved APIs for time/date support
Hare’s best-in-class time and date support has received numerous improvements and refinements over the course of this release cycle, mostly to simplify and clarify its usage. These changes are important in order to make it as easy as possible for Hare users to handle the tricky problems of time/date handling in an intuitive and maintainable way.
A small example of this change is in the adjustments of terminology around time zones in Hare. For example, time::date::zone was renamed to time::date::zonephase – this type is used to account for daylight savings time and other “phases” that a time zone experiences (expressing the releationship between, for instance, CEST and CET). Many similarly simple tweaks throughout the API make it easier to understand what your time/date code is doing and to avoid many common mistakes with time/date handling. By the way: the new hare-update tool includes rules to assist in accomodating all of these little renamings and refactorings in your downstream code.
This release also notably includes support for PETZ (POSIX Extending TZ) rules, which allows better support for projecting time zone phases and transitions into the future.
Semantic code annotations
This release also adds support for semantic code annotations, to enrich your Hare code with additional metadata. Here’s an example of how this is being used to prototype JSON encoders and decoders for hare-json:
#[json::gen]
export type player = struct {
name: str,
#[json::field(name = "X", omit_null=true)]
x: *f64,
#[json::field(name = "Y", omit_null=true)]
y: *f64,
};
Annotations consist of the “#[” token, an arbitrary identifier, and then a balanced series of Hare tokens (and “]” to finish it off). This “balanced” series of Hare tokens allows any series of lexical tokens, provided that each “(” token matches a “)” token and so on.
The Hare compiler discards all annotations, but the Hare standard library’s lexer implementation has been expanded to allow users to hook into these annotations to parse the metadata encoded in them – see hare::lex::register_annotation to get started.
Tooling for updating codebases affected by breaking changes
This release includes the optional hare-update tool to assist in updating codebases which are affected by breaking changes in Hare upstream. It’s recommended that you use this tool to assist in particular with dealing with the mandatory out-of-memory error handling requirements introduced by this release.
hare-update is based on a rules engine which pattern matches your Hare code against rules defined by Hare maintainers that cover the breaking changes introduced in each release, and presents you with suggested edits to your code to address them. An in depth introduction to hare-update was published to the Hare blog a couple of weeks ago – check it out for all of the details on this new tool.
Your downstream distribution has been asked to distribute hare-update as an optional secondary package – make sure to install it after you upgrade if you want to try it out.
Breaking changes
This release includes a number of breaking changes from 0.24.2, which may require downstream users to update their code. These are summarized in the release notes, which I’ve copied here for your convenience.
All memory allocations have been updated to return the new “nomem” type in case of failure (“nomem” is now a reserved keyword). Affected features include the alloc, insert, and append built-ins, as well as any standard library functions which allocate memory. Downstream users are required to update their code to handle these errors.
os::exec::lookup now returns an fs::error instead of void if the desired command could not be found.
time::chrono::moment now only holds an instant and timescale. The daydate, daytime, zone-offset, and locality fields are repurposed in time::date::date and handled there. Various interface changes have been made.
The following have been moved from time::chrono to time::date:
- invalidtzif
- tzdberror
- tz() (to be renamed to tzdb())
- locality
- timezone
- zone (to be renamed to zonephase)
- transition (to be renamed to zonetransition)
- ozone() (to be renamed to zone())
- zone_finish()
- daydate()
- daytime()
- from_datetime()
- in()
- fixedzone()
- coincident()
- simultaneous() (to be removed)
- EARTH_DAY
- MARS_SOL_MARTIAN
- MARS_SOL_TERRESTRIAL
- TAI
- UTC
- LOCAL
- GPS
- TT
- MTC
time::chrono::analytical has been removed, as it was not considered particularly useful.
time::chrono::convert now expects a time::chrono::moment instead of a time::instant.
The time::chrono::discontinuity error has been replaced with the more specific tscmismatch error. invalid was also removed.
time::date::unit has been renamed to step, and unitdiff renamed to hop. period has been renamed to span, and pdiff to traverse. peq was renamed to equalspan.
time::date::coincident now tests if the inputs have the same locality, not the same timescale.
time::date::simultaneous is removed in favor of time::chrono::compare.
time:: functions to work with Unix timestamps have been removed. To accomodate the removal, the following changes are required downstream:
- time::from_unix(42) -> time::new(42)
- time::unix(t) -> t.sec
time::date::fixedzone now allocates the return value, which the caller must free with timezone_free. The caller must also handle nomem.
The io::copier function type has been changed, changing the type of the “from” argument from io::stream to io::handle. Implementations of io::stream which provide a copier must be updated accordingly.
Additional information for downstream distributions
This Hare release includes changes which, as a downstream, you may have to make some changes to your packages to accomodate. In particular, the addition of the tool subcommand for hare(1) is likely to affect how you package Hare, and the release of hare-update calls for special attention as well.
The hare-tool subcommand works by executing programs in the libexecdir of the
installation prefix, so that hare tool example
will run e.g.
/usr/libexec/hare/hare-example
. You need to update your config.mk downstream
to include the following variables:
LIBEXECDIR = $(PREFIX)/libexec
TOOLDIR = $(LIBEXECDIR)/hare
The sample configs have been updated accordingly, for reference. You may customize these variables in the manner appropriate to your downstream needs.
It is also recommended that you package and ship the hare-update tool alongside this release. hare-update is an optional add-on which should be installed in a separate package. It follows the standard packaging conventions for Hare software and its versioning will track Hare upstream, with an additional point release number to track patches between Hare releases. The first version is 0.25.2.0.