Validating strings at compile time in Zig
Tags: zig
Published: 2020-08-05 21:39:36

The scenario is simple, and unfortunately quite common when interfacing with libraries designed for C: you have a library function which takes a single string parameter which can only contain a list of pre-defined values.

Take, for example, the [`pledge(2)`](https://man.openbsd.org/pledge.2) system call from OpenBSD. This system call is quite simple, and takes two parameters — both of which are strings that may only contain a pre-defined list of promises.

Passing a string with an invalid promise value (such as a spelling mistake) will result in an error at runtime. Wouldn't it be nice if we could instead catch such an issue at compile time?

That's exactly what we're going to do, using the [Zig](https://ziglang.org) programming language.

*Note: this is extracted from [a blog post on my site](https://www.euantorano.co.uk/posts/compile-time-string-validation/), which looks at some alternative approaches in a couple of other languages*

## What is Zig?

Zig is described as:

> Zig is a general-purpose programming language and toolchain for maintaining **robust**, **optimal**, and **reusable** software.

It is a language with a C-like syntax that aims to replace C. It brings some useful tools to the table, including comprehensive support for running code at compile time.

It's this functionality that we will be using in this post.

## Defining the problem

Our problem is going to take some queues from the `pledge(2)` system call, though we are going to simplify it a bit:

- In this case, the system call will only have 1 string parameter.
- In this case, there will be drastically fewer promises available:
    - stdio
    - rpath
    - wpath
    - path
    - dpath
- Our function signature should look something like the following:
    
    ```
    int pledge(const char *promiseList);
    ```
- The `promiseList ` parameter should be a string containing only the `promises` set of available promises, separated by a single space.

## Writing some Zig

Let's write some code!

### Creating an enum to represent the available promises

As we have a limited set of pre-defined promises, the easiest way to represent these values is as an enum.

Let's start off by defining one, named `Promises`:

```zig
const Promises = enum {
    stdio,
    rpath,
    wpath,
    path,
    dpath,
};
```

This should be quite familiar to most with some experience using c-like languages such as C# or Java.

### Writing a function to validate a string of promises

We should probably create a function that will take a string of promises separated by spaces and ensure all the values are valid, well-known values.

Luckily, Zig's standard library has some functions to help with this:

- [`std.mem.tokenize`](https://ziglang.org/documentation/0.6.0/std/#std;mem.tokenize) — to split a string by a particular delimiter.
- [`std.meta.stringToEnum`](https://ziglang.org/documentation/0.6.0/std/#std;meta.stringToEnum) — to parse a string value into an enum member.

We can use these functions to iterate through all sub-values in a `promises` string and try to parse them as members of the `Promises` enum:

```zig
const std = @import("std");
const tokenize = std.mem.tokenize;
const stringToEnum = std.meta.stringToEnum;

fn validatePromises(promises: []const u8) bool {
    var splitter = tokenize(promises, " ");

    while (splitter.next()) |s| {
        _ = stringToEnum(Promises, s) orelse return false;
    }

    return true;
}
```

If any of the values within the `promises` string fail to parse, we return `false`. However, if they all parse successfully, we return `true`.

### Calling `validatePromises` at compile time

This is where the magic happens. We now have a way to validate a promises string, but we need to call it at compile time.

Luckily, Zig has an awesome [`comptime`](https://ziglang.org/documentation/0.6.0/#comptime) concept. We can easily declare that certain arguments passed to functions should be known at compile time, and run whole blocks of code at compile time using those parameters. For example:

```zig
const warn = std.debug.warn;

fn promise(comptime promises: []const u8) void {
    comptime {
        if (!validatePromises(promises)) {
            @compileError("invalid promises: " ++ promises);
        }
    }

    warn("valid promises: {}", .{promises});
}
```

This code will call the `validatePromises` function at compile time with the passed in `promises` string. If the validation fails, a compiler error is emitted, including the invalid promises.

This results in a build failure like the following:

```
./src/main.zig:28:13: error: invalid promises: stdio foobar
            @compileError("invalid promises: " ++ promises);
            ^
./src/main.zig:37:16: note: called from here
        promise("stdio foobar");
               ^
./src/main.zig:35:29: note: called from here
pub fn main() anyerror!void {
                            ^
app...The following command exited with error code 1:

...
```

## Putting it all together

I've only scratched the surface of what's possible with Zig's `comptime` facilities here, but I hope I've given you some inspiration to give it a try.

I'd love to see what other ideas you think up with this kind of approach. For now, I've got [a simple repository](https://github.com/euantorano/compile-time-string-validation) available with the working source code for this Zig implementation, along with a [Nim](https://nim-lang.org) version and a [D](https://dlang.org) version.