Is there a stricter/slower/eager build mode?

Whenever I’m prototyping or experimenting, which is often, I run into things like this. Let’s say I’m experimenting with Number and the get function.

I’m not sure if this design is going to work, so I don’t want to write unit tests that 1. I’ll be changing frequently because I’m tweaking the design still or 2. I might completely throw away if this design doesn’t pan out.

Anyway, I have this code.

const std = @import("std");

const Number = enum {
    one,
    two,
};

fn get(n: Number) i32 {
    return switch (n) {
        .one => "1",
        .two => "2",
        .three => "3",
    };
}

pub fn main(init: std.process.Init) !void {
    _ = init;
}
$ zig build --watch
Build Summary: 3/3 steps succeeded
install success
watching 79 directories, 0 processes

Great! Zig was able to compile the code, so I guess that means I got the syntax and types correct.

Wait, wait. I remember something about Zig not building stuff if it’s not referenced or something… let me reference get from main. I usually have to do something like this to get all tests to run for some reason…

pub fn main(init: std.process.Init) !void {
    _ = init;
    _ = get;
}
$ zig build --watch
Build Summary: 3/3 steps succeeded
install success
watching 79 directories, 0 processes

Sweet! Still good. Awesome, let’s keep going!

1 hour later

Okokokok. I’m ready to commit to this design and connect it to the rest of my program. Let me actually call get from main now.

pub fn main(init: std.process.Init) !void {
    _ = init;
    get(.one);
}

Zig compiler: April fools!

   └─ compile exe tmp Debug native 2 errors
src/main.zig:12:10: error: enum 'main.Number' has no member named 'three'
        .three => "3",
        ~^~~~~
src/main.zig:3:16: note: enum declared here
const Number = enum {
               ^~~~

So. I guess Zig wasn’t really checking all of my code… it’s fast because it didn’t do anything?.. :sob:

Is there a way to tell Zig to slow down and actually check all of my code? I would be 100% OK with trading speed for actually knowing that my code builds. It kinda feels like a step back from C++ or Go, where compile == code builds/types are correct.

I’m currently in the middle of a medium-sized refactor. I’m slinging code around, writing new code on top, and I’m terrified none of it actually builds…

1 Like

I wouldn’t say it’s cheating.

Zig is just not doing any work it doesn’t have to in order to compile the program you asked for. If you don’t reference all the code, it doesn’t look at all the code, and it doesn’t differentiate between your code and any library code in that behavior (such as std). This is what’s called “lazy evaluation,” (evaluate when you need to) as opposed to “eager evaluation” (evaluate everything as soon as possible). They both result in the same program, but the latter is slower (and as you think you want, can fail to compile something that the program doesn’t even need).

That’s also not the only reason it’s fast, but it certainly helps.

Best way to check your work is to actually use your code as soon as possible, by referencing it in something you’re trying to compile, even if it’s just a test runner or, a throwaway Compile artifact.

1 Like

Although I sympathize, this is pretty embedded into the design of the language.

Consider something like this code:

const processSpawn = switch (native_os) {
    .wasi, .emscripten, .ios, .tvos, .visionos, .watchos => processSpawnUnsupported,
    .windows => processSpawnWindows, // Compile error on Linux
    else => processSpawnPosix, // Compile error on Windows
};

The language leverages this to enable multiple platform support. If it assessed all reachable code, nothing would compile at all (consider the use of @compileError builtin as well).

Edit:

I just realised I didn’t provide any advise to help. You could do a few things, but basically as you point out, you just need to make sure your code is reached. One thing you can do is some sort of test-driven development, where you write a test early with a trivial usage of your code (just a line that calls your code is enough). Then you can run zig build test --watch (Also ensure that zig build is actually running your tests)

7 Likes

Consider this modified version of your example:

const std = @import("std");
const builtin = @import("builtin");

const Number = switch (builtin.target.os.tag) {
    .windows => enum {
        foo,
        bar,
    },
    else => enum {
        one,
        two,
        three,
    },
};

fn getWindows(n: Number) i32 {
    return switch (n) {
        .foo => 1,
        .bar => 2,
    };
}

fn getPosix(n: Number) i32 {
    return switch (n) {
        .one => 1,
        .two => 2,
        .three => 3,
    };
}

pub fn main() !void {
    const result = switch (builtin.target.os.tag) {
        .windows => getWindows(.foo),
        else => getPosix(.one),
    };
    std.debug.print("{}\n", .{result});
}

This compiles and works just fine. However, this would never be able to compile for any target if getWindows was ever evaluated for non-Windows targets or getPosix was ever evaluated for Windows targets.

Relevant link:

7 Likes

This is a feature, not a bug (or cheat).

1 Like

Ah, you guys are right. It’s not cheating. I updated the title.

Also, yeah, I realize it’s not the only thing that makes it fast. I just took a break from coding and re-watched Andrew’s Practical DOD talk: Andrew Kelley - Practical DOD

A lot has gone into this.

1 Like

I actually like the idea of a build mode to go over compiling everything, so just in cases you don’t want to write a test but just want to know if your codes are legally compiled.

I went into this thing a couple times: oh, 1 error, let me fix that, then I will go to bed… 5:30 in the morning… If I knew there were 36 errors, I would just go to bed… :rofl:

1 Like

Good link, thanks! I found this quote from that link:

My mental model for Zig is that it’s just an interpreted language

Yeah, this makes sense. It feels like I’m writing Python/JavaScript where I don’t know if my code actually works until I hit it with tests.

I totally forgot about conditional compilation. Although, I wish there was a magic flag that could enable eager compilation for programs where I know for sure I don’t need any conditional magic.

1 Like

What’s a compile artifact?

The compilation output (the resulting binary)

There are, of course, various things you can do yourself in userland to make sure that declarations are analyzed during computation. One of them used to be in std called std.testing.refAllDeclsRecursive(); that’s code you could copy into your project and use if you need it.

Oh, duh. Sorry, I was thinking it was some object file or other intermediary binary. :sweat_smile:

an object file, or other intermediate binary is also a compile artifact, because they are the result of some compilation even if not complete.

1 Like