Truly @nogc Exceptions?

Steven Schveighoffer schveiguy at gmail.com
Wed Sep 19 21:16:00 UTC 2018


Given dip1008, we now can throw exceptions inside @nogc code! This is 
really cool, and helps make code that uses exceptions or errors @nogc. 
Except...

The mechanism to report what actually went wrong for an exception is a 
string passed to the exception during *construction*. Given that you 
likely want to make such an exception inside a @nogc function, you are 
limited to passing a compile-time-generated string (either a literal or 
one generated via CTFE).

To demonstrate what I mean, let me give you an example member function 
inside a type containing 2 fields, x and y:

void foo(int[] arr)
{
    auto x = arr[x .. y];
}

There are 2 ways this can throw a range error:

a) x > y
b) y > arr.length

But which is it? And what are x and y, or even the array length?

The error message we get is basic (module name and line number aren't 
important here):

    core.exception.RangeError at testerror.d(6): Range violation

Not good enough -- we have all the information present to give a more 
detailed message. Why not:

    Attempted slice with wrong ordered parameters, 5 .. 4

or

    Slice parameter 6 is greater than length 5

All that information is available, yet we don't see anything like that.

Let's look at the base of all exception and error types to see why we 
don't have such a thing. The part which prints this message is the 
member function toString inside Throwable, repeated here for your 
reading pleasure [1]:

     void toString(scope void delegate(in char[]) sink) const
     {
         import core.internal.string : unsignedToTempString;

         char[20] tmpBuff = void;

         sink(typeid(this).name);
         sink("@"); sink(file);
         sink("("); sink(unsignedToTempString(line, tmpBuff, 10)); 
sink(")");

         if (msg.length)
         {
             sink(": "); sink(msg);
         }
         if (info)
         {
             try
             {
                 sink("\n----------------");
                 foreach (t; info)
                 {
                     sink("\n"); sink(t);
                 }
             }
             catch (Throwable)
             {
                 // ignore more errors
             }
         }
     }

(Side Note: there is an overload for toString which takes no delegate 
and returns a string. But since this overload is present, doing e.g. 
writeln(myEx) will use it)

Note how this *doesn't* allocate anything.

But hang on, what about the part that actually prints the message:

         sink(typeid(this).name);
         sink("@"); sink(file);
         sink("("); sink(unsignedToTempString(line, tmpBuff, 10)); 
sink(")");

         if (msg.length)
         {
             sink(": "); sink(msg);
         }

Hm... Note how the file name, and the line number are all *members* of 
the exception, and there was no need to allocate a special string to 
contain the message we saw. So it *is* possible to have a custom message 
without allocation. It's just that the only interface for details is via 
the `msg` string member field -- which is only set on construction.

We can do better.

I noticed that there is a @__future member function inside Throwable 
called message. This function returns the message that the Throwable is 
supposed to display (defaulting to return msg). I believe this was 
inserted at Sociomantic's request, because they need to be able to have 
a custom message rendered at *print* time, not *construction* time [2]. 
This makes sense -- why do we need to allocate some string that will 
never be printed (in the case where an exception is caught and handled)? 
This helps alleviate the problem a bit, as we could construct our 
message at print-time when the @nogc requirement is no longer present.

But we can do even better.

What if we added ALSO a function:

void message(scope void delegate(in char[]) sink)

In essence, this does *exactly* what the const(char)[] returning form of 
message does, but it doesn't require any allocation, nor storage of the 
data to print inside the exception. We can print numbers (and other 
things) and combine them together with strings just like the toString 
function does.

We can then replace the code for printing the message inside toString 
with this:

        bool printedColon = false;
        void subSink(in char[] data)
        {
           if(!printedColon && data.length > 0)
           {
               sink(": ");
               printedColon = true;
           }
           sink(data);
        }
        message(&subSink);

In this case, we then have a MUCH better mechanism to implement our 
desired output from the slice error:

class RangeSliceError : Throwable
{
     size_t lower;
     size_t upper;
     size_t len;

     ...

     override void message(scope void delegate(in char[]) sink)
     {
         import core.internal.string : unsignedToTempString;

         char[20] tmpBuff = void;

         if (lower > upper)
         {
            sink("Attempted slice with wrong ordered parameters ");
            sink(unsignedToTempString(lower, tmpBuff, 10));
            sink(" .. ");
            sink(unsignedToTempString(upper, tmpBuff, 10));
         }
         else if (upper > len)
         {
            sink("Slice parameter ");
            sink(unsignedToTempString(upper, tmpBuff, 10));
            sink(" is greater than length ");
            sink(unsignedToTempString(len, tmpBuff, 10));
         }
         else // invalid parameters to this class
            sink("Slicing Error, but unsure why");
     }
}

And look Ma, no allocations!

So we can truly have @nogc exceptions, without having requirements to 
use the GC on construction. I think we should add this.

One further thing: I didn't make the sink version of message @nogc, but 
in actuality, it could be. Notice how it allocates using the stack. Even 
if we needed some indeterminate amount of memory, it would be simple to 
use C malloc/free, or alloca. But traditionally, we don't put any 
attributes on these base functions. Would it make sense in this case?

As Andrei says -- Destroy!

-Steve

[1] - 
https://github.com/dlang/druntime/blob/542b680f2c2e09e7f4b494898437c61216583fa5/src/object.d#L2642
[2] - https://github.com/dlang/druntime/pull/1895


More information about the Digitalmars-d mailing list