Jump to page: 1 2 3
Thread overview
Truly @nogc Exceptions?
Sep 19, 2018
Seb
Sep 20, 2018
Atila Neves
Sep 20, 2018
Atila Neves
Sep 20, 2018
H. S. Teoh
Sep 20, 2018
H. S. Teoh
Sep 20, 2018
Neia Neutuladh
Oct 19, 2018
Atila Neves
Oct 19, 2018
Nicholas Wilson
Oct 20, 2018
Atila Neves
Oct 21, 2018
welkam
Sep 20, 2018
Adam D. Ruppe
Sep 20, 2018
Adam D. Ruppe
Sep 20, 2018
Adam D. Ruppe
Oct 20, 2018
Paolo Invernizzi
Oct 20, 2018
Mike Parker
Oct 20, 2018
Paolo Invernizzi
Sep 21, 2018
Jonathan M Davis
Sep 21, 2018
Nemanja Boric
Sep 21, 2018
Nemanja Boric
September 19, 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@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
September 19, 2018
On 9/19/18 5:16 PM, Steven Schveighoffer wrote:
> 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?

Aand, no we can't. Because the sink could actually allocate.

-Steve
September 19, 2018
On Wednesday, 19 September 2018 at 21:28:56 UTC, Steven Schveighoffer wrote:
> On 9/19/18 5:16 PM, Steven Schveighoffer wrote:
>> One further thing: I didn't make the sink version of message @nogc, but in actuality, it could be.

We recently introduced support for output ranges in the formatting of Phobos:

https://dlang.org/changelog/2.079.0.html#toString

Output ranges have the advantage that they could be @nogc and because of the templatization also @safe.
September 19, 2018
On 9/19/18 7:53 PM, Seb wrote:
> On Wednesday, 19 September 2018 at 21:28:56 UTC, Steven Schveighoffer wrote:
>> On 9/19/18 5:16 PM, Steven Schveighoffer wrote:
>>> One further thing: I didn't make the sink version of message @nogc, but in actuality, it could be.
> 
> We recently introduced support for output ranges in the formatting of Phobos:
> 
> https://dlang.org/changelog/2.079.0.html#toString
> 
> Output ranges have the advantage that they could be @nogc and because of the templatization also @safe.

I don't think that will work here, as Throwable is a class.

All I can think of is that you would have 2 versions, a @nogc one that takes a @nogc delegate, and one that is not.

Of course, your exception type could define something completely separate, and you can deal with it in your own project as needed.

If there was a way to say "this is @nogc if you give it a @nogc delegate, and not if you don't", that would be useful. The compiler could verify it at compile-time.

-Steve
September 20, 2018
On Wednesday, 19 September 2018 at 21:16:00 UTC, Steven Schveighoffer wrote:
> 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).

<snip>

I expressed my concern for DIP1008 and the `msg` field when it was first announced. I think the fix is easy and a one line change to dmd. I also expressed this on that thread but was apparently ignored. What's the fix? Have the compiler insert a call to the exception's destructor at the end of the `catch(scope Exception)` block. That's it. The `msg` field is just a slice, point it to RAII managed memory and you're good to go.

Give me deterministic destruction of exceptions caught by scope when using dip1008 and I'll give you @nogc exception throwing immediately. I've even already written the code!
September 20, 2018
On 9/20/18 6:48 AM, Atila Neves wrote:
> On Wednesday, 19 September 2018 at 21:16:00 UTC, Steven Schveighoffer wrote:
>> 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).
> 
> <snip>
> 
> I expressed my concern for DIP1008 and the `msg` field when it was first announced. I think the fix is easy and a one line change to dmd. I also expressed this on that thread but was apparently ignored. What's the fix? Have the compiler insert a call to the exception's destructor at the end of the `catch(scope Exception)` block. That's it. The `msg` field is just a slice, point it to RAII managed memory and you're good to go.
> 
> Give me deterministic destruction of exceptions caught by scope when using dip1008 and I'll give you @nogc exception throwing immediately. I've even already written the code!

I thought it already did that? How is the exception destroyed when dip1008 is enabled?

But this means you still have to build msg when throwing the error/exception. It's not needed until you print it, and there's no reason anyway to make it allocate, even with RAII. For some reason D forces msg to be built, but it does't e.g. build the entire stack trace string before hand, or build the string that shows the exception class name or the file/line beforehand.

-Steve
September 20, 2018
On Thursday, 20 September 2018 at 12:48:13 UTC, Steven Schveighoffer wrote:
> On 9/20/18 6:48 AM, Atila Neves wrote:
>> On Wednesday, 19 September 2018 at 21:16:00 UTC, Steven Schveighoffer wrote:
>>> [...]
>> 
>> <snip>
>> 
>> I expressed my concern for DIP1008 and the `msg` field when it was first announced. I think the fix is easy and a one line change to dmd. I also expressed this on that thread but was apparently ignored. What's the fix? Have the compiler insert a call to the exception's destructor at the end of the `catch(scope Exception)` block. That's it. The `msg` field is just a slice, point it to RAII managed memory and you're good to go.
>> 
>> Give me deterministic destruction of exceptions caught by scope when using dip1008 and I'll give you @nogc exception throwing immediately. I've even already written the code!
>
> I thought it already did that? How is the exception destroyed when dip1008 is enabled?

I've had a 2 week holiday since I was working on this so I don't
quite remember. I'll try and find out the specifics for you later.
What I do remember is that the destructor didn't get called.

> But this means you still have to build msg when throwing the error/exception. It's not needed until you print it, and there's no reason anyway to make it allocate, even with RAII.

Maybe, but it's a lot easier to just allocate and have the destructor take care of it.

> For some reason D forces msg to be built, but it does't e.g. build the entire stack trace string before hand, or build the string that shows the exception class name or the file/line beforehand.

Good points.

September 20, 2018
On Thu, Sep 20, 2018 at 08:48:13AM -0400, Steven Schveighoffer via Digitalmars-d wrote: [...]
> But this means you still have to build msg when throwing the error/exception. It's not needed until you print it, and there's no reason anyway to make it allocate, even with RAII. For some reason D forces msg to be built, but it does't e.g. build the entire stack trace string before hand, or build the string that shows the exception class name or the file/line beforehand.
[...]

IIRC, originally the stacktrace was also built at exception construction time. But it was causing a major performance hit, so eventually someone changed the code to construct it lazily (i.e., only when the catcher actually tries to look it up).

I think it makes sense to also make .msg lazy, if the exception object is already carrying enough info to build the message when the catcher asks for it. And if the catcher doesn't ask for it, we saved an extra GC allocation (which is a plus even if we're not trying to go @nogc).


T

-- 
Computers shouldn't beep through the keyhole.
September 20, 2018
On 9/20/18 11:06 AM, H. S. Teoh wrote:
> On Thu, Sep 20, 2018 at 08:48:13AM -0400, Steven Schveighoffer via Digitalmars-d wrote:
> [...]
>> But this means you still have to build msg when throwing the
>> error/exception. It's not needed until you print it, and there's no
>> reason anyway to make it allocate, even with RAII. For some reason D
>> forces msg to be built, but it does't e.g. build the entire stack
>> trace string before hand, or build the string that shows the exception
>> class name or the file/line beforehand.
> [...]
> 
> IIRC, originally the stacktrace was also built at exception construction
> time. But it was causing a major performance hit, so eventually someone
> changed the code to construct it lazily (i.e., only when the catcher
> actually tries to look it up).
> 
> I think it makes sense to also make .msg lazy, if the exception object
> is already carrying enough info to build the message when the catcher
> asks for it. And if the catcher doesn't ask for it, we saved an extra GC
> allocation (which is a plus even if we're not trying to go @nogc).

Except we DON'T construct the stack trace string, even lazily. If you look at the code I posted, it's output directly to the output buffer (via the sink delegate), without ever having allocated.

I think we can do that for the message too (why not, it's all supported). But either one (using GC at print time, or lazily outputting to buffer at print time) solves the biggest problem -- being able to construct an exception without the GC.

Plus, this nudges developers of exceptions to store more useful data. If you catch an exception that has details in it, possibly it is only going to be in the string, which you now have to *parse* to get out what the problem was. If instead it was standard practice just to store the details, and then construct the string later, more useful information would be available in the form of fields/accessors.

Think about this -- every ErrnoException that is thrown allocates its message via the GC on construction. Even if you catch that and just look at the errno code. Even with dip1008:

https://github.com/dlang/phobos/blob/6a15dfbe18f9151379f6337f53a3c41d12dee939/std/exception.d#L1625

-Steve
September 20, 2018
On Wednesday, 19 September 2018 at 21:16:00 UTC, Steven Schveighoffer wrote:
> As Andrei says -- Destroy!

Nah, I agree. Actually, I'm of the opinion that string error messages in exceptions ought to be considered harmful: you shouldn't be doing strings at all. All the useful information should be in the type - the class name and the members with details.

Well, defining a new class can sometimes be a mild hassle... but for really common ones, we really should just do it, and other ones can be done as templated classes or templated factory functions that define a new class right there and then.

http://arsdnet.net/dcode/exception.d

That's the proof-of-concept I wrote for this years ago, go to the bottom of the file for the usage example. It uses a reflection mixin to make writing the new classes easy, and I even wrote an enforce thing that can add more info by creating a subclass that stores arguments to functions so it can print it all (assuming they are sane to copy like strings or value things lol)

	enforce!fopen("nofile.txt".ptr, "rb".ptr);

MyExceptionBase@exception.d(38): fopen call failed
        filename = nofile.txt
        mode = rb
----------------
« First   ‹ Prev
1 2 3