Do we need an @assert?

That’s what @TypeOfdoes though. It analyzes at compile-time an expression whose evaluation can depend on runtime.

And yes, the idea would be that the compiler should optimize stuff, providing that the expression would be true if it was evaluated. But it’s not actually evaluated (ie always optimized away).

For example @assert(runtime_stuff == 0) would allow the compiler to replace x * runtime_stuff with 0.

As I said before, I do think it doesn’t change much the current std.debug.assert. Because either the evaluation of runtime_stuff has side effects and the compiler can’t optimize much, or it doesn’t have side effects and the compiler would’ve optimized std.debug.assert anyway.

I believe that guaranteeing it is always optimized away would require the compiler to effectively do the same work as the optimization phase, but prior to the optimization phase (LLVM), which would be contrary to the Zig compiler’s performance requirements. Or for the compiler front-end and back-end to be integrated differently. But I’m not a compiler person so I don’t know for sure.

We do have guidelines here:

The TL;DR:

  • Control plane always asserts unconditionally, even if we do O(N^2) validation for O(N) work (because if N is large enough to be problematic, its data plane, not control plane)
  • In data plane, always assert unconditionally once before/after the hot loop. In the loop, conditionally assert if assert is measured to slow us down.
  • Judiciously use per-data structure verify flags, rather than a global one, for testing.
8 Likes

Excellent, thanks

I think it’s worth mentioning that No hidden control flow. is the first of the stated design goals of the language on https://ziglang.org/. That @assert built-in would be exactly that: hidden control flow. So this is not just about adding a function, but about breaking one of the main design goals of the language.

2 Likes

I’m confused. Could you please explain the hidden control flow you mean, and how this is different from the existing std.debug.assert?

std.debug.assert only guarantees the if branch will be removed in unsafe modes, the condition in the if may remain if it has side effects such as writing to memory that can be accessed from other places.

The suggestion for @assert is that it guarantees the removal of the condition even if it has side effects. This is hidden in that it isn’t normal zig semantics, it will be a special case you need to know.

2 Likes

Well, Ok. To me, although it has different behavior I don’t see it being a case of hidden control flow. If the problem is the name being the same/similar for both types, a different name could be used. I’m not arguing for @assert, but I think “no hidden control flow” (while a good principle) is being overused here.

I do agree, this isn’t really hidden control flow, at least not on the surface. If you think of it as adding a comptime if around the provided expression, then it is hidden control flow, and it is effectively a macro. Though I didn’t think of that perspective until now.

Regardless, the main issue people have with this is: it is inconsistent with the rest of the language.

The argument about the name is in favour of @assert/or changing the name of the existing std.debug.assert as that’s how similarly named things in other languages work, though they too are usually inconsistent with their language, unless it is a macro.

Which I think would be terrible anyway, an assert should not have side effects and be optimized away, or you’d have different states in different build modes, so the assertion would be valid only where you’d have safety checks anyway and you wouldn’t need it. If a condition brings side effects it should be handled with errors, not assertions, right?

3 Likes

The fact people want it shows that there are plenty of cases where the asserted condition has side effects, this is usually the result of getting the data you want to assert on in the first place.

for example, you have a database you are putting things in occasionally, but often what/if you put in the db depends on stuff thats already there. You dont want to read from it every time you want to write to it, so you keep whatever you need for that in memory. Now you have an assumption that your in memory data, and db data are in sync, to assert that assumption you need to read data from the db and compare it to what you have in memory. That operation has side effects, and exists only to validate your assumption. Failure of this condition is purely the fault of the programmer.

I think you are missing the point of assert, it is not to check things the language already checks, it’s to check things the language doesn’t. Zig doesn’t know what your minimum buffer size is so it can’t check that, zig doesn’t know foo should only be called under certain conditions.

assert is for your assumptions/conditions that can’t be encoded into the language.

whether a condition has side effects is irrelevant to whether it should be asserted or return an error.

If the failure of the condition is something the caller can/should be able to handle, then it should be an error.
If failure of the condition can be only the fault of the programmer, then it should be an assertion.

For example std.Io.Reader.rebase, only calls vtable.rebase under certain conditions, the default vtable.rebase asserts those conditions. Failure of that assert means the upper non vtable function didn’t implement the right logic, it is not the fault of the user of the interface, nor is it the concrete implementations (eg File.Reader) fault. It is the fault of andrew/core team member who made the interface.

I don’t think you are guaranteed to already have ‘different build states’ if you need an assert with side effects. However, I think you should have ‘different build states’ so you can configure what asserts you want without affecting separate categories of asserts. This is very useful for debugging.

1 Like

I think we need to settle on how an @assert would behave in safe and unsafe modes in this kind of scenario:

const boolean = runtimeWithSideEffects();
@assert(boolean);

Namely:

  • Can this panic when boolean is false?
  • Would if (!boolean) … be optimized away?
  • Would runtimeWithSideEffects still be called?

Because it’s not clear to me anymore what everyone wants, and I’m not sure it’s clear for many people either.

4 Likes

My opinion is simple: If it has side effects, it has to be called (or rather: the side effects have to happen). That’s what no hidden control flow means to me. Then, it also doesn’t matter if you have the call inside or outside the assert.

4 Likes

Just clarifying to others that you have described the current behaviour of std.debug.assert.

4 Likes

It has been answered to a degree, but I’ll clarify what I mean by hidden control flow. To cite the website again (this is from Overview ⚡ Zig Programming Language ):

If Zig code doesn’t look like it’s jumping away to call a function, then it isn’t.

This means that if you don’t see () anywhere, there will not be a function call (and thus no side effects can apply). The opposite is currently also true: If I do see a () somewhere and none of the keywords for controlling flow (if, for, …) are around, a function will be called (or its side effects will apply). One case of implicitly not calling a function is also explicitly mentioned on the same page, by the way:

C++, D, and Go have throw/catch exceptions, so foo() might throw an exception, and prevent bar() from being called.

While this is not the same as our @assert situation, it certainly has the same result: In @assert(foo()), I see a foo() and it might not be called. That is hidden control flow to me.

1 Like

Not true, see @TypeOf.

1 Like

True, that’s definitely an exception to this rule. I didn’t know this and for now I don’t really like it :smiley:
Thanks for pointing it out :+1:

1 Like

This thread is full of confused ideas.

It’s very intentional for assert in Zig to behave like a normal function, which gives you the most straight-forward behavior you can think of (it’s literally just a function like any other). People asking for a builtin are asking for magical behavior (which is what builtins give you) but IMO without a rationale why that would be better than what we currently have.

C-style ‘disappearing’ asserts are not going to be a thing in the Zig language, if you want that you can make your own abstraction based on a compile-time setting.

7 Likes

I agree. I’d be more interested in a @hasSideEffects that, like @TypeOf, wouldn’t actually run the expression it’s been given.

1 Like

No side affects I suggested.

The current assert has side effects. It could be used to optimize things and it could be optimized away.