Hare
Hare is a small compiled systems language with a C-like execution model, tagged unions for errors, and a standard library that ships with the compiler. Current release 0.26.0.1 (point release of 0.26.0, 2026-02-13); there is no 1.0 and the project states it has not reached its planned 1.0 freeze. Supported targets: x86_64, aarch64, riscv64 on Linux, FreeBSD, OpenBSD, NetBSD, and DragonFlyBSD. No generics, no macros, no package manager.
Contract
| Field |
Bound contract |
| Trigger |
The user writes, builds, tests, or evaluates Hare code, needs the C FFI pattern, or asks whether Hare fits a utility, daemon, or tool compared with C or Zig. |
| Authority |
Reversible local: writes only Hare source files, build outputs named by -o, and the toolchain install under the prefix the user chose; rollback is deleting those files or make uninstall in the toolchain clone. No remote mutation. |
| Side effect |
Source and binary files in the project directory. A toolchain bootstrap compiles qbe, harec, and hare and installs them under /usr/local by default. |
| Done |
The program builds with hare build, its tests pass under hare test, every error path is propagated with ? or handled in a match, and any C boundary is declared with the bodyless-prototype form and linked with -l. |
Inputs
- Source directory: a Hare module is a directory holding one or more
.ha files. Dependencies resolve by scanning use directives, first in the working directory and then along HAREPATH (a colon-separated list; hare version -v prints the default). There is no manifest file.
- Toolchain:
qbe (backend), harec (compiler), hare (build driver and standard library). A C11 compiler and scdoc (for man pages, optional) are the bootstrap dependencies.
- Build tags when a file is platform- or mode-specific:
+linux, +test, +libc in the file name or via -T.
- For C interop: the C library name for
-l and, when the header lives off the default path, -L.
Procedure
Install or confirm the toolchain. Check with hare version. To bootstrap: clone and make then make install harec from https://git.sr.ht/~sircmpwn/harec, then the same for hare from https://git.sr.ht/~sircmpwn/hare (make check runs its test suite first). Install qbe from https://c9x.me/compile/ before harec. Root is needed only because the default prefix is /usr/local. Done when: hare version prints a version.
Lay out the module. Create a directory and a main.ha:
use fmt;
export fn main() void = {
fmt::println("Hello, Hare!")!;
};
Build with hare build -o mytool and run with ./mytool, or hare run . to do both. Done when: the binary prints the greeting.
Handle errors as values. A fallible function returns a tagged union of its result and its error types. ? propagates an error to the caller; ! asserts success and aborts on error; match handles each case.
use fmt;
use fs;
use io;
use os;
use strings;
use encoding::utf8;
fn read_file(path: str) (str | fs::error | io::error | utf8::invalid) = {
const file = os::open(path)?;
defer io::close(file)!;
const buf = io::drain(file)?;
return strings::fromutf8(buf)?;
};
export fn main() void = {
match (read_file("config.txt")) {
case let s: str =>
fmt::println(s)!;
case let err: fs::error =>
fmt::fatalf("error: {}", fs::strerror(err));
case let err: io::error =>
fmt::fatalf("error: {}", io::strerror(err));
case utf8::invalid =>
fmt::fatal("config.txt is not UTF-8");
};
};
os::open returns (io::file | fs::error); os defines no error type of its own. io::drain allocates and reads to end of file; io::readall fills a caller-supplied buffer instead. strings::fromutf8 returns (str | utf8::invalid), never a bare str. Done when: the compiler accepts the function and every union member has a case.
Use the type system as written. Tagged unions: type color = (u8 | u16 | void);. Slices are pointer plus length: fn sum(nums: []i32) i32 with for (let i = 0z; i < len(nums); i += 1). Size literals carry the z suffix. There are no implicit numeric conversions; cast with v: u16. abort() and abort("message") end the program; fmt::fatalf prints and exits. Done when: no conversion is implicit and every match on a union is exhaustive or has a case => arm.
Cross the C boundary with a bodyless prototype. Import types::c for C-compatible types and declare the foreign function without a body; add @symbol("name") only when the Hare identifier must differ from the linker symbol:
use types::c;
export type FILE = opaque;
export @symbol("fopen") fn fopen(pathname: const *c::char, mode: const *c::char) nullable *FILE;
Export a Hare function to C with the same attribute in reverse: export @symbol("greet_user") fn greet_user(user: const *c::char) int = { ... };. Link with hare build -l <libname>; any -l also links libc and adds the +libc tag. There is no @extern attribute. Done when: the program links and calls across the boundary.
Write tests next to the code in +test.ha files with the @test attribute and assert:
@test fn sum_small() void = {
assert(sum([1i32, 2, 3]) == 6);
};
hare test adds the +test tag and runs them; a trailing glob selects tests by name. -v prints the compiler and linker commands, not per-test detail. Done when: hare test exits 0.
Reach for the standard library before writing code: fmt, io, os, fs, strings, bufio, memio, net (with net::tcp, net::udp, net::dial, net::dns, net::ip, net::uri), time (with time::date, time::chrono), encoding (base32, base64, hex, pem, utf8, asn1), types::c. JSON is not in the core library; hare-json is a separate extended-library repository providing encoding::json. Done when: no hand-written code duplicates a stdlib module.
Decide fit. Hare suits command-line utilities, build tools replacing shell, small network daemons, and code that must stay auditable without a runtime. Choose Zig instead when compile-time generics or metaprogramming carry the design (zig-cinterop covers its C boundary), and C when the project is glue over a large existing C code base (gcc). Done when: the recommendation names the deciding constraint.
Failure and recovery
| Failure |
Cause |
Fix |
unknown type or unresolved identifier |
Missing use module; or module not on HAREPATH |
Add the use line; check hare version -v for the search path |
| Link error on a C symbol |
Library not passed |
Add -l <libname> and, for a non-default path, -L <dir> |
| Compiler rejects an unhandled union member |
match not exhaustive |
Add the missing case or a case => arm; or propagate with ? |
utf8::invalid at runtime |
Bytes are not UTF-8 |
Handle the strings::fromutf8 error case instead of asserting with ! |
| Test not run |
File lacks the +test tag or function lacks @test |
Rename to name+test.ha and add the attribute |
| No stdlib module for a platform call |
Library gap |
Declare the libc function as a bodyless prototype and link with -l |
| Code from an older tutorial fails to compile |
Pre-1.0 churn; 0.26.0 replaced @offset with _ padding fields |
Read the release notes for the installed version |
Output
A Hare module that builds with hare build, passes hare test, propagates or handles every error, and declares any C boundary with the bodyless-prototype form and the -l flags recorded; plus, when the question was fit, a recommendation naming the deciding constraint.
1---2name: hare-lang3description: Use when building or evaluating a Hare program: hare build, run, or test, tagged-union error handling, or calling C through bodyless fn declarations and -l. Not for Zig or C: use zig-cinterop or gcc.4---56# Hare78Hare is a small compiled systems language with a C-like execution model, tagged unions for errors, and a standard library that ships with the compiler. Current release 0.26.0.1 (point release of 0.26.0, 2026-02-13); there is no 1.0 and the project states it has not reached its planned 1.0 freeze. Supported targets: x86_64, aarch64, riscv64 on Linux, FreeBSD, OpenBSD, NetBSD, and DragonFlyBSD. No generics, no macros, no package manager.910## Contract1112| Field | Bound contract |13|---|---|14| Trigger | The user writes, builds, tests, or evaluates Hare code, needs the C FFI pattern, or asks whether Hare fits a utility, daemon, or tool compared with C or Zig. |15| Authority | Reversible local: writes only Hare source files, build outputs named by `-o`, and the toolchain install under the prefix the user chose; rollback is deleting those files or `make uninstall` in the toolchain clone. No remote mutation. |16| Side effect | Source and binary files in the project directory. A toolchain bootstrap compiles `qbe`, `harec`, and `hare` and installs them under `/usr/local` by default. |17| Done | The program builds with `hare build`, its tests pass under `hare test`, every error path is propagated with `?` or handled in a `match`, and any C boundary is declared with the bodyless-prototype form and linked with `-l`. |1819## Inputs2021- Source directory: a Hare module is a directory holding one or more `.ha` files. Dependencies resolve by scanning `use` directives, first in the working directory and then along `HAREPATH` (a colon-separated list; `hare version -v` prints the default). There is no manifest file.22- Toolchain: `qbe` (backend), `harec` (compiler), `hare` (build driver and standard library). A C11 compiler and `scdoc` (for man pages, optional) are the bootstrap dependencies.23- Build tags when a file is platform- or mode-specific: `+linux`, `+test`, `+libc` in the file name or via `-T`.24- For C interop: the C library name for `-l` and, when the header lives off the default path, `-L`.2526## Procedure27281. Install or confirm the toolchain. Check with `hare version`. To bootstrap: clone and `make` then `make install` `harec` from `https://git.sr.ht/~sircmpwn/harec`, then the same for `hare` from `https://git.sr.ht/~sircmpwn/hare` (`make check` runs its test suite first). Install `qbe` from `https://c9x.me/compile/` before `harec`. Root is needed only because the default prefix is `/usr/local`. Done when: `hare version` prints a version.292. Lay out the module. Create a directory and a `main.ha`:3031 ```hare32 use fmt;3334 export fn main() void = {35 fmt::println("Hello, Hare!")!;36 };37 ```3839 Build with `hare build -o mytool` and run with `./mytool`, or `hare run .` to do both. Done when: the binary prints the greeting.403. Handle errors as values. A fallible function returns a tagged union of its result and its error types. `?` propagates an error to the caller; `!` asserts success and aborts on error; `match` handles each case.4142 ```hare43 use fmt;44 use fs;45 use io;46 use os;47 use strings;48 use encoding::utf8;4950 fn read_file(path: str) (str | fs::error | io::error | utf8::invalid) = {51 const file = os::open(path)?;52 defer io::close(file)!;53 const buf = io::drain(file)?;54 return strings::fromutf8(buf)?;55 };5657 export fn main() void = {58 match (read_file("config.txt")) {59 case let s: str =>60 fmt::println(s)!;61 case let err: fs::error =>62 fmt::fatalf("error: {}", fs::strerror(err));63 case let err: io::error =>64 fmt::fatalf("error: {}", io::strerror(err));65 case utf8::invalid =>66 fmt::fatal("config.txt is not UTF-8");67 };68 };69 ```7071 `os::open` returns `(io::file | fs::error)`; `os` defines no error type of its own. `io::drain` allocates and reads to end of file; `io::readall` fills a caller-supplied buffer instead. `strings::fromutf8` returns `(str | utf8::invalid)`, never a bare `str`. Done when: the compiler accepts the function and every union member has a `case`.724. Use the type system as written. Tagged unions: `type color = (u8 | u16 | void);`. Slices are pointer plus length: `fn sum(nums: []i32) i32` with `for (let i = 0z; i < len(nums); i += 1)`. Size literals carry the `z` suffix. There are no implicit numeric conversions; cast with `v: u16`. `abort()` and `abort("message")` end the program; `fmt::fatalf` prints and exits. Done when: no conversion is implicit and every `match` on a union is exhaustive or has a `case =>` arm.735. Cross the C boundary with a bodyless prototype. Import `types::c` for C-compatible types and declare the foreign function without a body; add `@symbol("name")` only when the Hare identifier must differ from the linker symbol:7475 ```hare76 use types::c;7778 export type FILE = opaque;79 export @symbol("fopen") fn fopen(pathname: const *c::char, mode: const *c::char) nullable *FILE;80 ```8182 Export a Hare function to C with the same attribute in reverse: `export @symbol("greet_user") fn greet_user(user: const *c::char) int = { ... };`. Link with `hare build -l <libname>`; any `-l` also links libc and adds the `+libc` tag. There is no `@extern` attribute. Done when: the program links and calls across the boundary.836. Write tests next to the code in `+test.ha` files with the `@test` attribute and `assert`:8485 ```hare86 @test fn sum_small() void = {87 assert(sum([1i32, 2, 3]) == 6);88 };89 ```9091 `hare test` adds the `+test` tag and runs them; a trailing glob selects tests by name. `-v` prints the compiler and linker commands, not per-test detail. Done when: `hare test` exits 0.927. Reach for the standard library before writing code: `fmt`, `io`, `os`, `fs`, `strings`, `bufio`, `memio`, `net` (with `net::tcp`, `net::udp`, `net::dial`, `net::dns`, `net::ip`, `net::uri`), `time` (with `time::date`, `time::chrono`), `encoding` (`base32`, `base64`, `hex`, `pem`, `utf8`, `asn1`), `types::c`. JSON is not in the core library; `hare-json` is a separate extended-library repository providing `encoding::json`. Done when: no hand-written code duplicates a stdlib module.938. Decide fit. Hare suits command-line utilities, build tools replacing shell, small network daemons, and code that must stay auditable without a runtime. Choose Zig instead when compile-time generics or metaprogramming carry the design (`zig-cinterop` covers its C boundary), and C when the project is glue over a large existing C code base (`gcc`). Done when: the recommendation names the deciding constraint.9495## Failure and recovery9697| Failure | Cause | Fix |98|---|---|---|99| `unknown type` or unresolved identifier | Missing `use module;` or module not on `HAREPATH` | Add the `use` line; check `hare version -v` for the search path |100| Link error on a C symbol | Library not passed | Add `-l <libname>` and, for a non-default path, `-L <dir>` |101| Compiler rejects an unhandled union member | `match` not exhaustive | Add the missing `case` or a `case =>` arm; or propagate with `?` |102| `utf8::invalid` at runtime | Bytes are not UTF-8 | Handle the `strings::fromutf8` error case instead of asserting with `!` |103| Test not run | File lacks the `+test` tag or function lacks `@test` | Rename to `name+test.ha` and add the attribute |104| No stdlib module for a platform call | Library gap | Declare the libc function as a bodyless prototype and link with `-l` |105| Code from an older tutorial fails to compile | Pre-1.0 churn; 0.26.0 replaced `@offset` with `_` padding fields | Read the release notes for the installed version |106107## Output108109A Hare module that builds with `hare build`, passes `hare test`, propagates or handles every error, and declares any C boundary with the bodyless-prototype form and the `-l` flags recorded; plus, when the question was fit, a recommendation naming the deciding constraint.