# Hare First Impressions Some months ago, ddevault (a hacker I have a great deal of respect for) and a few other folks released Hare, a new systems programming language. This was exciting news and I immediately put it on my list of languages to try out and then promptly forgot. Tonight, I unexpectedly found myself with a free evening so I decided to see what it felt like. I know it's still early in development and some stuff is definitely Not Done Yet, but these are my first impressions of the language as of today. => https://harelang.org ## Installation It depends only on qbe, scdoc, and a C compiler. Since I already had a C compiler I only needed to install the other two of these, and they are both blessedly well-behaved and default to installing into /usr/local, so all that took was: $ wget https://c9x.me/compile/release/qbe-1.0.tar.xz $ tar xvf qbe-1.0.tar.xz $ cd qbe-1.0 $ make $ sudo make install $ git clone https://git.sr.ht/~sircmpwn/scdoc $ cd scdoc $ make $ sudo make install ## Aside 1: What's qbe & why does hare depend on it? | qbe is a standalone compiler backend, which takes programs written in a low- | level language intended to be generated by compilers and produces optimized | machine code. It's like llvm if llvm could be understood and modified by | humans. Since it handles both the fiddly parts of generating machine code | for various architectures and also contains good versions of a bunch of | difficult optimizations, depending on it lets the hare compiler focus on the | stuff that is actually novel and interesting without first reproducing | hundreds of klocs worth of compiler research about SSA form. => https://c9x.me/compile ## Aside 2: What's scdoc & why does hare depend on it? | scdoc is a tool that generates man pages from a markup language. I don't | know for sure but I believe it exists because ddevault does not want to | write {,g,n}roff by hand. => https://git.sr.ht/~sircmpwn/scdoc With deps in place, the next step is to fetch and build the hare bootstrap compiler. Like many language implementations, hare is mostly written in itself, but it needs a compiler written in an existing language (in this case C) so that a user without an existing Hare compiler can build their first binary. The bootstrap compiler is similarly easy, and once again it default installs to /usr/local: $ git clone https://git.sr.ht/~sircmpwn/harec $ cd harec $ make $ make check $ sudo make install Now, onward to the Real Stuff. We grab the actual build driver and such: $ git clone https://git.sr.ht/~sircmpwn/hare $ cd hare $ cp config.example.mk $ vi config.mk $ make $ make check $ sudo make install Note: this does install to /usr, unlike the other packages mentioned which install to /usr/local[5]. I changed this since I wanted it in /usr/local. Since I was building on an AArch64 host, not an x86-64 one, I also had to make the following edits to config.mk: * Set ARCH = aarch64 instead of x86_64 * Change the prefixes in the AARCH64_* variables below, since config.mk assumes that if you're building for aarch64 you are cross-compiling[1]. I did not, at first, notice that I needed to do either of those things, so I initially ran make directly with an unmodified config.mk, which obediently built a handful of x86 binaries (!) before bailing out with complaints from the assembler about x86 instruction mnemonics. After that, I neglected to make clean (oops) and confused myself for a little while. Don't do that. Once I got that sorted out, hare installed cleanly! ## Hello, World! I was able to compile and run a "Hello, World" program: $ hare run hello.ha Hello, World! Yay! However, when I tried using the build tool: $ hare build hello.ha I was surprised that there seemed to be no output binary. In fact, hare's default output binary name is the basename of the current directory if you do this, so I ended up with a binary named "scratch". So, note to self for the future: you need the -o argument to `hare build`. ## Haredoc As I was working in Hare, I leaned very heavily on `haredoc`, which is a tool that ships as part of the language toolchain. It spits out documentation for any module in your $HAREPATH (including the entire standard library), which is pretty helpful: $ haredoc io::read // Reads up to len(buf) bytes from a [[handle]] into the given buffer, returning // the number of bytes read. fn read( h: handle, buf: []u8, ) (size | EOF | error); ... but it seems to disagree profoundly with the /usr/bin/less that ships with busybox and therefore with alpine, so if you're on an alpine host it's probably better to do: $ PAGER=cat heredoc io::read so that you will get colorized output and not a screen full of '~'. The official documentation page is also very good, although it is currently pretty light on higher-level conceptual documentation and heavier on module- and function-level stuff. The rendered haredocs for the entire stdlib are on the web at: => https://docs.harelang.org The higher-level documentation, which is still a bit in progress, lives at: => https://harelang.org/documentation/ ## My First Program This was written partly from docs and partly from experimentation. If you want to extract it to a standalone file, do: $ sed -e '1,/^>>>/d' -e '/^<< sha256sum.ha I've commented the parts that I found new or different as an experienced C/C++ programmer, and not bothered commenting stuff that I found obvious. >>> use crypto::sha256; use encoding::hex; use fmt; use io; use os; // Symbols have default private visibility instead of public, so you need to // mark them exported. Also, main() has a different signature than is // traditional: it doesn't take argv, nor does it return an exit code. If you // want the argv you need os::args. export fn main() void = { let h = sha256::sha256(); // Writing this isn't legal: // let b: [4096]u8 = []; // because everything needs to be explicitly initialized with values. let b: [4096]u8 = [0...]; for (true) { // Error handling in Hare! Many functions return a union of // whatever their normal success result is and an error code, // often an io::error or similar. That makes this pattern, // of matching on the result of such a function, very common. match (io::read(os::stdin, b)) { case let n: size => io::write(&h.stream, b[..n])!; case io::EOF => break; // These cases are required to be both exhaustive and non- // overlapping, so if you miss one the compiler will yell at // you[3]. case io::error => fmt::fatalf("ow!"); }; }; // This is a very C-style out parameter: we stack allocate a buffer big // enough for a sha256 hash, then call the sum function out of the // sha256 struct we created earlier to fill the finished hash into that // buffer. let r: [sha256::SIZE]u8 = [0...]; h.sum(&h, r); // encoding::hex::encode() actually returns size | io::error, and // io::error isn't allowed to be discarded/ignored. If we instead wrote // hex::encode(os::stdout, r); // (note the lack of '!' at the end), we'd get: // "cannot ignore error here". // There is a syntactic shorthand for turning those errors into runtime // fatalities, though - the '!' postfix operator turns the compile-time // requirement to handle errors into a runtime crash if an error is // unhandled. This saves you from having to write matches or verbose // error handling all over the place, as long as you are fine with the // crash. hex::encode(os::stdout, r)!; fmt::println()!; }; <<< ## Debugging I was pleasantly surprised to find that you can just debug hare programs in gdb normally: $ hare build -o hello hello.ha $ gdb ./hello [lots of gdb chatter] (gdb) break fmt.println Breakpoint 1 at 0x8020c78: file /tmp/0bc440f4aa950893/temp.fmt.46.s, line 2188. Pretty sweet! However, the debug info is still a little incomplete: (gdb) run Starting program: /home/elly/p/hello Breakpoint 1, fmt.println () at /tmp/0bc440f4aa950893/temp.fmt.46.s:2188 2188 /tmp/0bc440f4aa950893/temp.fmt.46.s: No such file or directory. But, whatever, at least if it crashes you can sorta figure out where, and that's half the battle. DWARF support is apparently planned, which will give you a lot more context when debugging. => https://todo.sr.ht/~sircmpwn/hare/42 The open bug for DWARF support ## Overall Impressions of Hare I quite like it! I'm very much approaching it from the perspective of a long time C/C++ programmer, and what I see here is a language that is a lot more capable and provides a lot more programmer support than C, but a lot more understandable than C++, and whose entire toolchain one could feasibly read and comprehend[4]. That's pretty great, and I really like the focus on ergonomics that I can already see in the project. There are a couple of things that have been tripping me up. One is that ';' is required in places it isn't in C, like after control structures, and another is that conversions between strings and byte vectors are more explicit - you can't just take a u8 and pretend it's a string or vice versa[2]. The standard library is also nice and already pretty full-featured, but I found it kind of hard to navigate. For example, if you want to read or write a file, those functions are io::{read,write}, but to *open* a file, you use os::open. I get conceptually why this is - the io module deals in abstract IO handles and opening an actual filesystem file is a concrete, platform-specific thing - but it still surprised me because other languages so often group open/read/write/close together. I also keep tripping over one quirk (from my perspective) of Hare style, which is that one writes: let x: int; instead of: let x : int; as I was taught to do in Standard ML many years ago. Oh well :P Also, I really like the approach to error handling. The whole match pattern I showed an example of feels kind of verbose at first, but it's not too bad: match (read(...)) { case let x: int => yield x; case io::error => fmt::fatal("oh no!"); }; Compare to C: if ((int x = read(...)) < 0) panic("oh no!"); The '!' postfix operator also handles the single most common case, which is "if this fails please kill my entire program", which is what you want to do a lot of the time if you aren't writing a library. There also exists the companion '?' postfix operator, which allows you to write: let x = foo(...)?; meaning "if foo returns a value of error type, return that value from this function, otherwise the expression has the returned value from foo" - kind of similar to writing this in C: if ((int x = foo(...)) < 0) return x; /* x now has the value of foo(...) in non-error cases */ That's it for now folks. Thanks for reading! I am sure I will keep using Hare, so probably there are more posts to come :D Also: many thanks to ecs for reading this over for correctness! You can read their excellent blog at: => https://ecs.d2evs.net => gemini://ecs.d2evs.net [1]: I'm guessing the developers mostly work on x86-64 hosts. [2]: This is a bad habit everyone has from the "ASCII or get out" days, but fortunately UTF-8 is a thing now and so every language other than C has more principled string handling. UTF-8 support is something a lot of small software doesn't bother with, so I'm really glad Hare gets it right. [3]: This isn't implemented yet and the first version of this program didn't handle the io::error case. Oops :) [4]: Except for binutils, which currently has no replacement. :( [5]: This got fixed immediately after I made this post, so you won't need to do that.