[Roc] How Our Rust-to-Zig Rewrite is Going

Richard’s style of writing is less similar to mine than Andrew’s or Mitchell’s: he reads as a bit more generous and takes time to say things that I might just leave to a reader to notice.

Excellent move, to move up this blog post ahead of the 0.1.0 announcement, given current events :wink:

Anyway, all in all a great read. Interesting insights into the way a project’s design helps it fit into one language better or worse than another. After all this talk about tracing GCs, I’m positively itching to write a language with one in Zig and see why it’s so fiendishly difficult!!

47 Likes

I see stuff like this and then suddenly it seems tantalizing fun:

Granted, these tricks are sort of a like “rest of the fucking owl” situation, and I’m sure the rest of the owl bites, but optimizations like this are really cool.

3 Likes

As discussed earlier, having full control over allocations and deallocations is what I want in our compiler’s implementation. And in tests, I also appreciate the testing allocators detecting leaks—it can even detect leaks in compiled Roc code! Unfortunately, to get that benefit requires a lot of “init this, defer deinit” code in tests that has to be correct or else the test fails on a memory leak. None of that is necessary in Rust. I care more about the compiler’s implementation being the way I want it than the tests looking nicer, but in a perfect world I could somehow have both.

For this purpose I normally just create a local arena for the test local data which I need as parameters and the like for the the actual function I want to test.

8 Likes

Sounds cool - could you share an example?

I don’t really have a simple example, but I guess this one works? This test isn’t supposed to check for leaks or anything else, just that it creates the correct argv for spawning a process.

test createCreateArgv {
    var helper_arena = std.heap.ArenaAllocator.init(std.testing.allocator);
    defer helper_arena.deinit();
    const helper_allocator = helper_arena.allocator();
    // setup
    const mounts = [_]container.Mount{
        container.Mount{
            .destination = "/test",
            .source = "/test",
            .kind = .{ .devpts = .{} },
            .options = .{ .rw = true },
            .propagation = .none,
        },
    };

    var env = std.process.EnvMap.init(helper_allocator);
    const env_key = "XDG_RUNTIME_DIR";
    const env_val = "/run/hi";
    try env.put(env_key, env_val);

    const img = b: {
        var img: Image = undefined;
        img.id = "hello";
        break :b img;
    };

    const entrypoint_argv = [_][]const u8{
        "test",
        "test",
    };

    const key = "key";

    const name = "name";

    const expected = &create_base ++ &[_][]const u8{
        name,
        "--env",
        env_key ++ "=" ++ env_val,
        "--label",
        label ++ "=" ++ key,
        "--mount=type=devpts,destination=" ++ mounts[0].destination ++ ",ro=false,exec",
        img.id,
    } ++ entrypoint_argv;

    // do
    const args = try createCreateArgv(helper_allocator, .{
        .entrypoint_argv = &entrypoint_argv,
        .env = env,
        .image = img,
        .mounts = &mounts,
        .key = key,
        .name = name,
    });

    // check
    outer: for (expected) |e| {
        for (args) |a| {
            if (std.mem.eql(u8, e, a)) {
                continue :outer;
            }
        } else {
            const stderr = std.debug.lockStderrWriter(&.{});
            defer std.debug.unlockStderrWriter();
            stderr.print("missing value: {s}\nhad: {{", .{e}) catch {};
            if (args.len > 0) {
                stderr.writeAll(args[0]) catch {};
                for (args[1..]) |a| {
                    stderr.print(", {s}", .{a}) catch {};
                }
            }
            stderr.writeAll("}\n") catch {};
            return error.TestValueNotFound;
        }
    }
}

The output to stderr is to make debugging easier since the test failed, but it’s kinda hard to know which value is wrong. (This codebase is still on 0.15.2.)

1 Like

Was going through the Roc code and it seems that my prediction was right. Due to the std.posix removal, people are now resorting to this and only mainstream systems are being supported, even though this could be trivially done as POSIX/Windows split if the wrappers stayed in the library. It’s a real shame that people will have to reimplement this stuff over and over again.

6 Likes

i think your prediction is still too early to be called, much as you seem to want your sour grapes validated in every thread :slight_smile:

2 Likes

Thanks for sharing! It seems like having to init/deinit an allocator for each test is exactly what Richard was lamenting, so I wonder if you could do that setup/teardown only once for a whole set of tests, to cut down on boilerplate?

(I’m new to Zig - only just started ziglings - so forgive me if that’s a naïve question!)

You could (by putting it into a separate function), but from my experience you rarely have much shared setup and teardown code between tests.