Paul Smith

"Writing an Interpreter in Go" in Zig, part 3

This post is part of the "Writing an Interpreter in Go" in Zig series.

Continuing with section 1.5, let’s add a REPL. Since all we’ve done so far is implement the lexer, the REPL will be limited to reading in source from stdin and printing a debugging representation of the token stream.

Fixed static buffer for reading in lines to avoid dealing with allocators

When I think about reading in a line from a stream in a C-like language, I immediately think about what memory will be used. I could allocate on the heap, but I don’t yet feel like passing allocators around. Since the REPL just needs enough memory to hold the most recent line typed in by the user, and since it’s source code it won’t be very long, we can just set aside a big static buffer and reuse it each time through the reading loop.

This is very easy in Zig, since the stdlib provides a method on types implementing the io.Reader “interface”, readUntilDelimiterOrEof(), which takes a slice and a delimiter byte (the newline here). We can declare a large array of u8 as a global variable and pass it as a slice to this method.

Here is the entirety of the REPL:

const std = @import("std");
const Lexer = @import("Lexer.zig");

const PROMPT = ">> ";

var line_buf: [4096]u8 = undefined;

pub fn start(reader: anytype, writer: anytype) void {
    while (true) {
        writer.print("{s}", .{PROMPT}) catch unreachable;
        const line = (reader.readUntilDelimiterOrEof(&line_buf, '\n') catch unreachable) orelse break;
        var lexer = Lexer.init(line);
        while (true) {
            const token = lexer.next();
            if (token.token_type == .eof) {
                break;
            }
            writer.print("{any}\n", .{token}) catch unreachable;
        }
    }
}

Equivalent of Go’s user.Current() to get the current username

I looked quickly at implementation of Go’s user.Current(). It makes a getuid() syscall, and then reads the pw file via getpwuid_r(), using Cgo.

There does not seem to be a Darwin or macOS wrapper for getuid() syscall or the getpwuid_r() libc function in Zig’s stdlib, so I decided to just write a simple C wrapper and import it into Zig.

I double-checked the function signature via man getpwuid on my machine. Since this is a POSIX function as well, this solution should be portable across platforms.

Since the username is just a very short-lived string we need at startup, I chose to use the simpler getpwuid() function instead, which just returns a pointer to a struct with all the relevant fields filled out for me. The docs say the space for this struct is a static buffer that will be overwritten on the next call to that function, but since we’re the only one to call it and we only call it once, I’m fine just returning the C string directly here.

The wrapper is as simple as this C file.

#include <unistd.h>
#include <pwd.h>

const char *getusername()
{
    struct passwd *pw;
    pw = getpwuid(geteuid());
    return pw->pw_name;
}

And here’s how to import it and call it from Zig:

// src/main.zig
// ...
const c = @cImport({
    @cInclude("user.h");
});

pub fn main() !void {
    // ...
    const username = c.getusername();
    try out.writer().print("Hello, {s}! This is the Monkey programming language!\n", .{username});
    // ...
}

We need to update our build.zig to know about the C wrapper. I just put it in a header file under “c-rsrc” directory relative to the root of my project.

// build.zig
// ...
// pub fn build(b: *std.Build) void {
// ...
const c_src = .{ .src_path = .{
    .owner = b,
    .sub_path = "c-src",
} };
exe.addIncludePath(c_src);
exe_unit_tests.addIncludePath(c_src);
// ...
// }

This concludes chapter 1. Next up, chapter 2, parsing.


This series