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@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

Reply via email to