On the subject of error messages

Steven Schveighoffer via Digitalmars-d digitalmars-d at puremagic.com
Mon May 15 12:44:11 PDT 2017


On 5/15/17 1:16 PM, Stanislav Blinov wrote:
> On Monday, 15 May 2017 at 15:30:38 UTC, Steven Schveighoffer wrote:
>
>>>> Argument 2 is a string, which is not supported
>>>> file(line): Error: template foo cannot deduce function from argument
>>>> types !()(int, string), candidates are:
>>>> file(line): foo(Args...)(auto ref Args arg) if (noStringArgs!args)
>>
>> I think the compiler should be able to figure this out, and report it.
>> The if constraint is a boolean expression, and so it can be divided
>> into the portions that pass or fail.
>
> How? The constraint, any constraint, is de-facto user code.

Code evaluated at compile time. It actually has to evaluate each of the 
pieces, and knows why the whole if statement fails exactly.

The constraint:

void foo(T)(T t) if (cond1!T && cond2!T && cond3!T), the compiler knows 
both what each of those terms evaluate to, and therefore which ones are 
causing the thing not to be enabled.

> Even in the
> simple example I've provided, I would not expect the compiler to figure
> out what are *my* expectations on the types.

I think you misunderstand, your example would still not compile, and 
instead of "here are all the ones I tried", it's "here are all the ones 
I tried, and in each case, I've highlighted why it didn't work".

Many times, you can figure out by looking at the constraints why it 
didn't work. However, I've encountered many cases where it's saying it 
doesn't pass isSomeDoodad!T when I thought T is a doodad. Then I need to 
figure out why it's not working.

Even your library code cannot be all-knowing about what the calling user 
is trying to do. It may be confusing to him as well. But just "here's a 
giant if statement, I as the compiler have figured out why it's not 
true, see if you can too!" is crap.

> Even if the compiler was to divide the constraint into blocks and reason
> about them separately,

It is.

> it's still limited to error reporting we have
> now: it will report "is(T == string) was expected to be false, but it's
> true". Is that a good error message?

Yes. It's a perfect error message actually. What is confusing about it?

>
>> What I'd love to see is the constraint colorized to show green
>> segments that evaluate to true, and red segments that evaluate to
>> false. And then recursively show each piece when asked.
>>
>> I think any time spent making a user-level solution will not scale.
>> The compiler knows the information, can ascertain why it fails, and
>> print a much nicer error message. Plus it makes compile-time much
>> longer to get information that is already available.
>
> I don't see how that is possible. The constraints' complexity is
> arbitrary, it's semantics are arbitrary. The compiler does a full
> semantic pass, we end up with the error messages as if it was normal
> program code. But the thing is, we need different error messages,
> because it isn't "normal" program code.

It has to know. It has to evaluate the boolean to see if it should 
compile! The current situation would be like the compiler saying there's 
an error in your code, but won't tell you the line number. Surely it knows.

>> Imagine also a constraint like isInputRange!R. This basically attempts
>> to compile a dummy lambda. How would one handle this in user-code?
>
> Umm... Exactly as it is implemented currently? With one important
> distinction that I would be able to report *exactly why* the type in
> question does not satisfy the constraint. Not an obscure
>
> "Error: no property 'empty' for type (typename)"
> "Error: expression 'foo.front()' is void and has no value"
>
> but a descriptive
>
> " Argument <number> does not satisfy the constraint isInputRange:"
> "(typename) is expected to be an input range, but it doesn't implement
> the required interface:"
> "   property 'empty' is not defined."
> "   property 'front' is defined, but returns void."

The first would be great. I'm having trouble really seeing a reason to 
prefer the second over the first, it's just a verbose description of the 
same thing.

Today we get an error that:

void foo(R)(R r) if(isInputRange!R)

doesn't compile for the obvious (to you) range type R. What it doesn't 
tell you is anything about why that doesn't work. We don't even get the 
"no property empty" message.

Let me give you a real example. The isForwardRange test used to look 
like this:

template isForwardRange(R)
{
      enum isForwardRange = isInputRange!R && is(typeof(
      (inout int = 0)
      {
          R r1 = R.init;
          static assert (is(typeof(r1.save) == R));
      }));
}

Here is the definition of isInputRange:

template isInputRange(R)
{
     enum bool isInputRange = is(typeof(
     (inout int = 0)
     {
         R r = R.init;     // can define a range object
         if (r.empty) {}   // can test for empty
         r.popFront();     // can invoke popFront()
         auto h = r.front; // can get the front of the range
     }));
}

Simple, right? However, this was the situation before I applied a fix:

struct ForwardRange
{
    int front() { return 1; }
    void popFront() {}
    enum empty = false;
    ForwardRange save() { return this; }
}

static assert(isInputRange!R);
static assert(!isForwardRange!R);

You tell me, what is the issue? Having library-writer defined error 
messages are not going to help there because I didn't do it *exactly* right.

If you aren't sure what the answer is, here is the PR that fixed it: 
https://github.com/dlang/phobos/pull/3276

> User code can collect all the information and present it in a readable
> way. Compiler will never be able to. The best the compiler would do is
> report "T does not satisfy isInputRange". And in my opinion, that is
> what it should do. Adding complexity to the compiler to figure out all
> imaginable variations doesn't seem like a very good idea. User code is
> able to make all assessments it needs, it just doesn't have the ability
> to elaborate.

No, the compiler just needs to detail its evaluation process that it's 
already doing.

If the constraint doesn't actually match the pragma message, you get 
MISLEADING messages, or maybe messages when it actually compiles. Much 
better to have the compiler tell you actually what it's doing.

>
> Another example:
>
> is(typeof(x) == string) && x.startsWith("_")
>
> The best we can expect from the compiler is print that out and say it
> evaluated to false.

It can say that is(typeof(x) == string) is false, or x.startsWith("_") 
is false.

> User code, on the other hand, can generate a string "x must be a string
> that starts with an underscore". Which one is better?

My version. Is x not a string, or does x not start with an underscore? 
Not enough information in your error message. And it doesn't give me 
more information than the actual constraint code, it's just written out 
verbosely.

-Steve


More information about the Digitalmars-d mailing list