#Handling enum with safe mode-only fields

1 messages · Page 1 of 1 (latest)

unreal rampart
#

I have an enum where some of the fields are available only in safe mode:

const std = @import("std");

const E = if (std.debug.runtime_safety)
    enum { a, b, c, d }
else
    enum { a, c };

fn f(comptime e: E) void {
    switch (e) {
        .a => {},
        .c => {},
        else => if (std.debug.runtime_safety)
            switch (e) {
                .b => {},
                .d => {},
            },
    }
}

pub fn main() void {
    _ = f(.a);
}

Trying to compile the above code in non-safe builds yields an error

src\main.zig:12:14: error: unreachable else prong; all cases already handled
        else => if (std.debug.runtime_safety)

Which options do I have? Defining all enum values regardless of the build mode is not a good idea, since those are also used to automatically construct VTables and there would be entries which should not be there, leading to instantiations of safe mode-only handlers (which I'd like to prevent).

amber sedge
#

maybe non-exhaustive enum can work for your case

#

so your enum becomes

const E = if (std.debug.runtime_safety)
    enum { a, b, c, d }
else
    enum { a, c, _ };
unreal rampart
#

Yeah, but that'd technically allow the enum take illegal values... And those would have to be handled.

amber sedge
#

technically, but since an explicit cast has to be there, the caller kinda asks for it

unreal rampart
#

The handling code will become awkward. Since this is a framework, some of the handling code will have to be written by the user.

amber sedge
#

you can also assert in the else branch that you have std.debug.runtime_safety == true

unreal rampart
#

The above is an example. Futher details would take lots of code. As I said, there's automated construction of the VTables (and semi-automated of handlers).

#

Effectively the function f is to become a handler prototype and would be auto-instantiated per enum value.

#

This function may be written by the framework user

#

So it has to be readable and writable 😉

amber sedge
#
fn f(comptime e: E) void {
    switch (e) {
        .a => {},
        .c => {},
        else => {
            std.debug.assert(std.debug.runtime_safety);
            switch (e) {
                .b => {},
                .d => {},
            }
        },
    }
}

this is good enough, right?

unreal rampart
#

Nope, since the b and d prongs might fail to instantiate. They should not be instantiated at all in non-safe builds. And the VTable entries are defined by the enum field availablility.

#

Which means I cannot auto-define VTable entries by the enum fields, which makes the framework API way more bloated.

amber sedge
#

I can't imagine how you auto-define the VTable so I can't discuss more unless you have some small snippet

unreal rampart
#

Roughly speaking I have a function makeVTable(E, f)

#

It instantiates f for each enum value and creates a VTable entry per such instantiation (the entries are in an array).

amber sedge
#

I understand that, but I'm imaging that you just switch through the fields in the enum, so something like

inline for (std.meta.fields(E)) |field| {
  f(field);
}

should work, but i guess that's not what you have in mind

unreal rampart
#

Not exactly that, but for our discussion here I guess your example is kinda equivalent.

#

Now the problem is, in non-safe builds an attempt to instantiate f with .b or .d might fail.

#

That's why the code is behind a comptime if in my original example above

#

(The reason for failure is that the functionality that need to be handled in b or d prongs is simply not available in non safe-builds)

amber sedge
#

yeah, but if it's always gated behind comptime, non-exhaustive enums work since an assert will fail at comptime anyway, right?

unreal rampart
#

They won't fail in debug builds

#

I mean the assert won't fail

#

Which means it won't fail ever 😄

amber sedge
#

isn't that what you want?

#

because the condition is runtime_safety?

unreal rampart
#

How does your "final proposal" look like, I'm a bit lost as to where we are

amber sedge
#

sorry, i'll write out the whole thing

#
const E = if (std.debug.runtime_safety)
    enum { a, b, c, d }
else
    enum { a, c, _ };

fn f(comptime e: E) void {
    switch (e) {
        .a => {},
        .c => {},
        else => {
            std.debug.assert(std.debug.runtime_safety);
            switch (e) {
                .b => {},
                .d => {},
            }
        },
    }
}
unreal rampart
#
  • As assert is not inline, it will be actually compiled as a runtime code, so it'll have to fail at runtime, which it won't.
  • It still allows out-of-range enum values (might be acceptable, but not really nice), and those would need to be handled
  • This might be acceptable in framework code, but this also might be a user function instead.
#

Actually this kind of assert is kinda noop

#

Because it succeeds in safe builds and does nothing otherwise

#

So again, the situation is probably mostly complicated by the factor that I want this code to be writable by the framework user, otherwise I could have arranged something more convoluted.

amber sedge
#

As assert is not inline, it will be actually compiled as a runtime code, so it'll have to fail at runtime, which it won't.
If you force e to always be comptime this fails at comptime, so i don't think it's a problem. If you don't, and the user is running without runtime safety, I think the whole branch just gets pruned out (just guessing, will need to inspect the generated output).

It still allows out-of-range enum values (might be acceptable, but not really nice), and those would need to be handled
It kinda does, but like I mentioned before, without runtime safety, the user has to be really ask for with something like @enumFromInt(3), and at that point, they kinda ask for it?

This might be acceptable in framework code, but this also might be a user function instead.
I will need a concrete example to imagine it better

unreal rampart
#

If you force e to always be comptime this fails at comptime, so i don't think it's a problem.
How's that?

  • Assert is not inline function, it won't be evaluated at comptime unless explicitly forced to
  • This assert can never fail anyway, so it doesn't do anything at all

It kinda does, but like I mentioned before, without runtime safety, the user has to be really ask for with something like @enumFromInt(3)
True, but a bigger problem is that the user is still forced to write the code to handle those (even those do not arrive)
I will need a concrete example to imagine it better
Yeah, there probably will need to be many more details. Maybe we rather stop here. Thanks for your effort and your time, maybe that'll give me some further ideas.

amber sedge
#

Assert is not inline function, it won't be evaluated at comptime unless explicitly forced to
please fact check me, but I believe in this case, Zig will run the assert at comptime, since Zig will run everything it can at comptime, and runtime_safety is comptime known, hence the assert will run at comptime also

unreal rampart
#

I did check it myself a while ago, so, unless my memory fails me...

#

That's why I always write comptime assert(..., if I need it at comptime

amber sedge
#

good practice anyway

unreal rampart
#

I probably could do with

const E = enum {
    a,
    b,
    c,
    d,

    pub fn safeModeOnly(self: @This()) bool {
        return switch (e) {
            .b, .d => true,
            else => false,
        };
    }
};

Let's see. Thanks for the discussion again.