What should happen when the assert message expression throws?

Petar Petar
Fri Nov 18 13:11:26 UTC 2022


On Friday, 18 November 2022 at 12:36:14 UTC, RazvanN wrote:
> I have stumbled upon: 
> https://issues.dlang.org/show_bug.cgi?id=17226
>
> I want to fix it, however, the solution is not obvious.
>
> Take this code:
>
> ```d
> import std.string;
>
> void foo(int i)
> {
>     // In this case a %s is forgotten but it could be any other 
> trivial error.
>     assert(i == 42, format("Bad parameter:", i));
> }
>
> void main()
> {
>     foo(43);
> }
> ```
>
> If `format` throws, then the Exception is thrown masking the 
> assert error.
> Unfortunately, I don't see a clear way to rewrite this to valid 
> D code as to catch the exception and then assert. Ideally, we 
> could do something along the lines of:
>
> ```d
> assert(i == 42,
>        (const(char)[] msg,
>         try { msg = format("Bad parameter:", i); },
>         catch (Exception e) { msg = "Assert message evaluation 
> has failed";},
>         msg)
>        );
> ```
>
> This rewrite would be done only if it is determined that the 
> assert message may throw. However, the current dmd-fe 
> implementation does not allow for such a construction and I 
> think that it might be overkill to implement the necessary 
> machinery just to support this case.
>
> The try catch block can also be generated outside of the assert 
> expression:
>
> ```d
> auto msg;
> try { msg = format("Bad parameter:", i);}
> catch (Exception e) { msg = "Assert message evaluation has 
> failed";}
> assert(i == 42, msg);
> ```
>
> The difference between the 2 is that in this case we are 
> evaluating the msg regardless of whether the assert condition 
> is true or false.
>
> Also, we need to take into account the case where the user 
> tries to catch the exception by himself:
>
> ```d
> void foo(int i)
> {
>     try
>     {
>         // In this case a %s is forgotten but it could be any 
> other trivial error.
>         assert(i == 42, format("Bad parameter:", i));
>     }
>     catch(Exception e) { /* do some sort of fix up with the 
> message */}
> }
>
> void main()
> {
>     foo(43);
> }
> ```
>
> Today, this code runs successfully and no AssertError is 
> thrown. If we automatically catch the exception we might break 
> such code (although I would argue it would not be too dramatic).
>
> An alternative solution would be to deprecate having an assert 
> message that may throw. This has the advantage that it avoids 
> complexity inside the compiler and the user is forced to write:
>
> ```d
>
> auto msg = /* do whatever you want with throwing code */
> assert(cond, msg);
>
> ```
>
> If the user wants to catch the exception or not, it's his/hers 
> business, but then the compiler has defined semantics in all 
> situations.
>
> What do you think? Is deprecating having an assert message that 
> may throw a severe restriction? Are there other rewrites that I 
> am missing?
>
> Cheers,
> RazvanN

Perhaps the `assert` hook in druntime could have additional 
overloads:

```
// pseudocode

// existing assert hook (without `-checkaction=context`)
void _d_assert(bool cond, string msg);

// new hook overloads
void _d_assert(bool cond, string function() /* infer attributes 
*/ lazyMessage);
void _d_assert(bool cond, string delegate() /* infer attributes 
*/ lazyMessage);
```

The compiler would select the lazyMessage overloads if the the 
expression could throw, or perhaps deemed more computationally 
expensive (based on some heuristic).

That way, we delay the evaluation of msg until we know for sure 
that the condition is false and then we could wrap that 
evaluation in `try`-`catch` and throw a proper nested `Throwable`.

If anything, making the `msg` parameter lazy evaluated would make 
the asserts more consistent with 
[`enforce`](https://dlang.org/phobos/std_exception#enforce).


More information about the Digitalmars-d mailing list