Proposal to add builtin @illegal(x: anytype) void

I propose to add a builtin function that looks something like this:
@illegal(x: bool) void
EDIT: actually i meant the following signature: @illegal(x: anytype) void
It asserts that the given expression triggers safety checked illegal behavior.
It’s a no-op in unchecked built modes.

Furthermore, this builtin could have arguments to indicate which type of illegal behaviour should trigger, in which function …

I’m not sure if an idea similiar to this one was already discussed, please give a link in that case.

std.debug.assert(x: bool) void also triggers safety checked illegal behavior and is no-op in unchecked built modes. How this one will be different?

1 Like

From quick search i found this discussion which sounds similar to what you are proposing Proposal : builtin @assert() One assert to rule them all

1 Like

no, my proposal does not assert that the expression is true, but that the expression itself triggers illegal behaviour somewhere. (the argument x could actually be of any type)

ie. if the expression does not trigger illegal behaviour then this builtin triggers illegal behviour otherwise does nothing.

In what situations would you use @illegal()?

to document behaviour. just like you do with normal asserts, but to ensure expected illegal beviour is actually illegal.
eg. in a test:
// this documents that key must be absent from the map for a call to insert_unique(key, val) to be legal.
map.insert(key, value1);
@illegal(map.insert_unique(key, value2));
// this documents that array.get_last() is illegal behaviour if array.length() == 0
array.clear();
assert(array.length() == 0);
@illegal(array.get_last())

In these cases I would do assert(map.contains(key)) or assert(array.items.len == 0). Do you have situations, where you would use @illegal(), where an assert is not possible.

No, because for @illegal to work there the respective safety checks / assertions must already be implemented.
However I argue that this builtin would make expressing some intent simpler without duplicating the assertion code.
assertions check that code that is supposed to be legal is legal and is semantically correct.
@illegal() checks that code that is supposed to be illegal is illegal.
ie. assertions check normal code and @illegal checks the assertion.
you could think of them as meta assertions. dont know if this makes sense though.

one use case i have in mind:
currently api users have to read documentation or internal source code to know the assumptions of the api. violating these assumptions would be illegal behaviour.
with my proposed builtin is is possible to document those assumptions with code tests from the outside.
again im unsure about the usefulness.

So your looking for something to assert that the branch may reach an unreachable or @panic?

The name is misleading then. There is no way to catch unchecked illegal behavior. @willPanic() fits better.

You would only use it in tests. In regular program it would mean a panic recovery which Zig does not want to have.

At this point why would you need a builtin? Can’t a custom test runner make it happen?

What they want is a new keyword in addition to test:

test_panic { // pass
  unreachable;
}

test_panic { // fail
  return;
}

If I understand this correctly, the goal is to make this valid?

test "" {
    const max: i32 = std.math.maxInt(i32);
    std.testing.expect(@illegal(max + 1));
}

Do you have a concrete example of this being needed, or preferable to an existing assert/comptime check?

Hmmm. You have a function that:

  1. Takes input.
  2. Can’t handle certain combinations of input.
  3. Should be optimized (in unchecked release modes) with the assumption it will never receive such input.
  4. You want automated tests to verify that this assumption is being communicated to the compiler.

That’s a very specific set of requirements, so I don’t think it would come up often. But I think you can do it with everything already available in zig:


Let’s say you need a function that takes two integers and does a very complex mathematical calculation that could potentially overflow, but you know it never will.

  1. Write the function with unreachable in the appropriate places
fn addCannotOverflow(comptime T: type, a: T, b: T) T) {
    if (T == comptime_int) return a + b;
    const ov = @addWithOverflow(a, b);
    if (ov[1] != 0) unreachable;
    return ov[0];
}
  1. Replace each unreachable with return #### an error if your choice. It doesn’t matter which.
/// Returns the sum of a and b. Returns an error on overflow.
fn add(comptime T: type, a: T, b: T) (error{Overflow}!T) {
    if (T == comptime_int) return a + b;
    const ov = @addWithOverflow(a, b);
    if (ov[1] != 0) return error.Overflow;
    return ov[0];
}
  1. Now you can write tests to verify that it returns an error:
test "testOverflow" {                                                           
     const max: i32 = std.math.maxInt(i32);                                      
     try std.testing.expectEqual(error.Overflow, add(i32, max, 1));              
 }
  1. Write a wrapper function that turns errors into checked illegal behaviour instead:
pub fn addCannotOverflow(comptime T: type, a: T, b: T) T {
    return add(T, a, b) catch unreachable;
}

There you have it, checked illegal behaviour that is verified by automated tests.

3 Likes