www.digitalmars.com         C & C++   DMDScript  

digitalmars.D - On the subject of error messages

reply Stanislav Blinov <stanislav.blinov gmail.com> writes:
Let's suppose I wrote the following template function:

import std.meta;

enum bool isString(T) = is(T == string);

void foo(Args...)(auto ref Args args)
if (!anySatisfy!(isString, Args)) {
    // ...
}
This one is variadic, but it could as well have been non-variadic. The important aspect is that it has a constraint. In this case, the constraint is that it should accept any argument types *but* strings. Now, if I call it with a string argument:
foo(1, "a");
I get the following error:
file(line): Error: template foo cannot deduce function from 
argument types !()(int, string), candidates are:
file(line): foo(Args...)(auto ref Args arg) if 
(!anySatisfy!(isString, Args))
Ok, so the call does not compile, but the message is rather vague: it doesn't tell me which argument(s) failed to satisfy the constraint. In this simple example it's easy to see where the error is, but if foo() was called in a generic way (i.e. arguments come from somewhere else, their type determined by inference, etc.), or if the constraint was more complex, it wouldn't be as easy to spot. So, to help with this, let me write a checker and modify foo's signature, thanks to CTFE:
template types(args...) {
    static if (args.length)
        alias types = AliasSeq!(typeof(args[0]), 
 types!(args[1..$]));
    else
        alias types = AliasSeq!();
}

auto noStringArgs(args...)() {
    import std.format;
    // use types, as otherwise iterating over args may not 
 compile
    foreach(i, T; types!args) {
        static if (is(T == string)) {
            pragma(msg, format!"Argument %d is a string, which 
 is not supported"
                    (i+1));
            return false;
        }
    }
    return true;
}

void foo(Args...)(auto ref Args args)
if (noStringArgs!args) {
    // ...
}
Now if I call foo() with a string argument, I get this:
foo(1, "a");


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)
That's a little bit better: if foo() fails to compile, I get a hint on which argument is incorrect. However, as you probably can tell, this doesn't scale. If later I decide to provide an overload for foo() that *does* accept string arguments, I'm going to see that message every time a call to foo() is made. What if we allowed constraint expressions, in addition to a type convertible to bool, return a Tuple!(Bool, Msgs), where Bool is convertible to bool, and Msgs is a string[]? Then my checker could be implemented like this:
auto noStringArgs(args...)() {
    import std.format;
    import std.typecons;
    string[] errors;
    foreach(i, T; types!args) {
        static if (is(T == string)) {
            errors ~= format!"Argument %d is a string"(i+1));
        }
    }
    if (errors) return tuple(false, ["This overload does not 
 accept string arguments"] ~ errors);
    return tuple(true, errors.init);
}
So it would accumulate all concrete error messages for the signature, and prefix them with a general descriptive message. When resolving overloads, the compiler could collect strings from such tuples, and if the resolution (or deduction, in case of single overload) fails, print them as error messages:
foo(1, "a", 3, "c");


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):
file(line):    This overload does not accept string arguments
file(line):    Argument 2 is a string, which is not supported
file(line):    Argument 4 is a string, which is not supported
And in case of overloads:
auto noNumericArgs(args...)() {
    import std.format;
    import std.typecons;
    import std.traits : isNumeric;
    string[] errors;
    foreach(i, T; types!args) {
        static if (isNumeric!T) {
            errors ~= format!"Argument %d (%s) is a string"(i+1, 
 T.stringof));
        }
    }
    if (errors) return tuple(false, ["This overload does not 
 accept numeric arguments"] ~ errors);
    return tuple(true, errors.init);
}

void foo(Args...)(auto ref Args args)
if (noStringArgs!args) { /* ... */ }

void foo(Args...)(auto ref Args args)
if (!noStringArgs!args && noNumericArgs!args) { /* ... */ }

foo(1, 2);     // ok, no error, first overload
foo("a", "b"); // ok, no error, second overload
foo(1, "b", "c");   // error


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):
file(line):    This overload does not accept string arguments
file(line):    Argument 2 is a string
file(line):    Argument 3 is a string
file(line): foo(Args...)(auto ref Args arg) if 
(!noStringArgs!args && noNumericArgs!args):
file(line):    This overload does not accept numeric arguments
file(line):    Argument 1 (int) is numeric
This would clearly show exactly for what reason each overload failed. You can imagine for complex template functions (i.e. likes of std.concurrency.spawn, std.getopt, etc) this could help convey the error much more concisely than just saying "hey, I failed, here are the candidates, go figure it out...". A crude implementation of this is possible as a library: https://dpaste.dzfl.pl/0ba0118c3cd9 but without language support, it'll just riddle the compiler output with messages on every call, regardless of the success of overload resolution, so the only use for that would be in case of no overloads. And the messages are ordered before compiler errors, which is less than helpful. Another idea, instead of using tuples, introduce a stack of messages for each overload, and allow a special pragma during constraint evaluation:
bool noStringArgs(args...)() {
    import std.format;
    import std.typecons;
    foreach(i, T; types!args) {
        static if (is(T == string)) {
            pragma(overloadError, format!"Argument %d is a 
 string"(i+1)));
            // may return early or continue to collect all errors
            // return false;
        }
    }
    return true;
}
pragma(overloadError, string) will "push" an error onto message stack. After evaluating noStringArgs!args, the compiler would check the stack, and if it's not empty, discard the result (consider it false) and use the strings from that stack as error messages. Trying to call noStringArgs() outside of constraint evaluation would result in compiler error (pragma(overloadError, string) should only be available in that context). There are other alternatives, e.g. there's a DIP by Kenji Hara: https://wiki.dlang.org/User:9rnsr/DIP:_Template_Parameter_Constraint The approach I'm proposing is more flexible though, as it would allow to evaluate all arguments as a unit and infer more information (e.g. __traits(isRef, args[i]). Constraint on every argument won't allow the latter, and would potentially require writing more explicit overloads. What do you guys think? Any critique is welcome, as well as pointers to alternatives, existing discussions on the topic, etc.
May 13
next sibling parent Stanislav Blinov <stanislav.blinov gmail.com> writes:
Nobody read that or is it just *that* bad? :)
May 15
prev sibling parent reply Steven Schveighoffer <schveiguy yahoo.com> writes:
On 5/13/17 10:41 AM, Stanislav Blinov wrote:
 Let's suppose I wrote the following template function:

 import std.meta;

 enum bool isString(T) = is(T == string);

 void foo(Args...)(auto ref Args args)
 if (!anySatisfy!(isString, Args)) {
    // ...
 }
This one is variadic, but it could as well have been non-variadic. The important aspect is that it has a constraint. In this case, the constraint is that it should accept any argument types *but* strings. Now, if I call it with a string argument:
 foo(1, "a");
I get the following error:
 file(line): Error: template foo cannot deduce function from argument
 types !()(int, string), candidates are:
 file(line): foo(Args...)(auto ref Args arg) if (!anySatisfy!(isString,
 Args))
Ok, so the call does not compile, but the message is rather vague: it doesn't tell me which argument(s) failed to satisfy the constraint. In this simple example it's easy to see where the error is, but if foo() was called in a generic way (i.e. arguments come from somewhere else, their type determined by inference, etc.), or if the constraint was more complex, it wouldn't be as easy to spot. So, to help with this, let me write a checker and modify foo's signature, thanks to CTFE:
 template types(args...) {
    static if (args.length)
        alias types = AliasSeq!(typeof(args[0]), types!(args[1..$]));
    else
        alias types = AliasSeq!();
 }

 auto noStringArgs(args...)() {
    import std.format;
    // use types, as otherwise iterating over args may not compile
    foreach(i, T; types!args) {
        static if (is(T == string)) {
            pragma(msg, format!"Argument %d is a string, which is not
 supported"
                    (i+1));
            return false;
        }
    }
    return true;
 }

 void foo(Args...)(auto ref Args args)
 if (noStringArgs!args) {
    // ...
 }
Now if I call foo() with a string argument, I get this:
 foo(1, "a");


 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. 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. Imagine also a constraint like isInputRange!R. This basically attempts to compile a dummy lambda. How would one handle this in user-code? I think there are several forum threads about diagnosing constraint issues, haven't got the time right now to look for them. -Steve
May 15
parent Stanislav Blinov <stanislav.blinov gmail.com> writes:
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. 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 provide code for doing that, the language gives me means to that effect. What it doesn't give me though is a way to cleanly report an error. Even if the compiler was to divide the constraint into blocks and reason about them separately, 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?
 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. In fact, what truly doesn't scale is the binary "is/isn't" solution we have now. Again, even if the compiler would display at which line/column `false` was inferred, it's not good enough, as it simply leaves the user to figure out what went wrong, without any clear hint.
 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." 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. 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. User code, on the other hand, can generate a string "x must be a string that starts with an underscore". Which one is better?
May 15