On the subject of error messages

Stanislav Blinov via Digitalmars-d digitalmars-d at puremagic.com
Sat May 13 07:41:50 PDT 2017


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.


More information about the Digitalmars-d mailing list