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 2017
next sibling parent Stanislav Blinov <stanislav.blinov gmail.com> writes:
Nobody read that or is it just *that* bad? :)
May 15 2017
prev sibling next 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 2017
next sibling parent reply 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 2017
parent reply Steven Schveighoffer <schveiguy yahoo.com> writes:
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
May 15 2017
parent reply Stanislav Blinov <stanislav.blinov gmail.com> writes:
On Monday, 15 May 2017 at 19:44:11 UTC, Steven Schveighoffer 
wrote:
 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.
Yes, that is what it is: code. Code that follows language rules, not some ADL. No mater how expressive the language is, it is still code. And if, on compile error, the compiler shows me the code from the foreign library that I thought I was using correctly, I'm supposed to now do what the compiler just did and figure out where I made a mistake.
 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". [...] [...] Then I need to figure out why it's not working.
It is exactly for the cases where the logic is more complex than a simple test that my proposal is for. I'm not suggesting to abuse it throughout.
 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?
There is no context. To get at context, I have to look at the code.
 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.
It "knows" it evaluated false. It doesn't know how to give user a digestible hint to make that false go away.
 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.
Exactly my point.
 Let me give you a real example. The isForwardRange test used to 
 look like this:

 template isForwardRange(R)
I'm going to need to digest that.
 No, the compiler just needs to detail its evaluation process 
 that it's already doing.
That is not enough. Failure may be X levels deep in some obscure chunk surrounded by static ifs and and a bunch of wonky tests involving attempted lambda compilations (which I'd have to parse and "compile" in my head in order to try to understand what failed).
 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.
The library author is free to take as many passes over their messages as they deem necessary. They can be as vague or as precise as needed. It is their responsibility.
 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.
I've already demonstrated that the message text can be made as precise as is required for a concrete use case, there's no need to nitpick ;) I could as easily report either: "x is not a string" or "string x should start with an underscore". or "Argument 10 (int) is not a string. This method is only callable with strings that start with an underscore". "Value of argument 10 (string) does not start with an underscore. This method is only callable with strings that start with an underscore". I, as a responsible author, can decide what amount of information must be presented. It could be accompanied by code, I'm not against that. The main point is to explain the error without *requiring* the user to use their head as a compiler.
May 15 2017
parent reply Steven Schveighoffer <schveiguy yahoo.com> writes:
On 5/15/17 4:24 PM, Stanislav Blinov wrote:
 On Monday, 15 May 2017 at 19:44:11 UTC, Steven Schveighoffer wrote:
 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.
It "knows" it evaluated false. It doesn't know how to give user a digestible hint to make that false go away.
I'm going to snip away pretty much everything else and focus on this. The compiler absolutely 100% knows, and can demonstrate, exactly why a template constraint failed. We don't have to go any further, or make suggestions about how to fix it. Just output what exactly is wrong, even if you have to recurse into the depths of some obscure template isXXX, and all it's recursively called templates, I can get the correct determination of where either my type isn't right, or the constraint isn't right. Essentially, the compiler can write "good enough" messages such that both an IDE and a person can understand it. That's all we need. We don't need to translate is(T == string) into "T should be a string" for people to get the meaning. All we get today is "it didn't work". We can do better: "it didn't work because ...", and all existing template constraints magically get better error messages. -Steve
May 15 2017
parent reply Stanislav Blinov <stanislav.blinov gmail.com> writes:
On Monday, 15 May 2017 at 20:55:35 UTC, Steven Schveighoffer 
wrote:
 On 5/15/17 4:24 PM, Stanislav Blinov wrote:
 On Monday, 15 May 2017 at 19:44:11 UTC, Steven Schveighoffer 
 wrote:
 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.
It "knows" it evaluated false. It doesn't know how to give user a digestible hint to make that false go away.
I'm going to snip away pretty much everything else and focus on this. The compiler absolutely 100% knows, and can demonstrate, exactly why a template constraint failed. We don't have to go any further, or make suggestions about how to fix it.
In complex constraints, that is not enough. When we have loops (i.e. over arguments, or over struct members), would it report the iteration/name? Would it know to report it if `false` came several levels deep in the loop body? Would it know that we actually *care* about that information? (*cough* C++ *cough* pages and pages of error text because of a typo...) When we have nested static ifs, it's important to see, at a glance, which parts of the combination were false. Again, if they're several &&, || in a row, or nested, pointing to a single one wouldn't in any way be informative. When we have tests using dummy lambdas, are we to expect users to immediately extract the lambda body, parse it, and figure out what's wrong?
 Just output what exactly is wrong, even if you have to recurse 
 into the depths of some obscure template isXXX, and all it's 
 recursively called templates, I can get the correct 
 determination of where either my type isn't right, or the 
 constraint isn't right.
Please look over my isMovable example (I'm not sure if you caught it, I posted it as a follow up to my other reply). Suppose the `false` is pointed at by the compiler:
    else static if (is(T == struct) &&
            (hasElaborateDestructor!T || 
 hasElaborateCopyConstructor!T)) {
        foreach (m; T.init.tupleof) {
            static if (!isMovable!(typeof(m)) && (m == m.init)) {
                return false;
                       ^
                       |
            }
        }
        return true;
    } else
That is very, *very* uninformative. I don't know which member it was, I don't know which part of the conditional was false. I don't know which part of the conditional further up was true. Would the compiler know to tell me all that? Would it know to test further, to collect *all* information, so that I don't have to incrementally recompile fixing one thing at a time? Most importantly, as a user who sees this for the first time, I'd have no idea *why* those checks are there. I'd have no context, no grounds to base my reasoning on, so I'd either have to jump back to docs to see if I missed a corner case, or start spelunking code that I didn't write, which is always so fun... Thing is, the compiler is exactly in that position. It doesn't read the docs, ever :) It's always spelunking code written by someone else. It can't tell what the constraint, as a unit, is *actually* testing for. It doesn't care that we shouldn't destructively move structs with const members. So it wouldn't be able to tell me either. All it will do is report me that that false was returned on that line, and (hopefully), some additional info, like member type and name.
May 15 2017
parent reply Steven Schveighoffer <schveiguy yahoo.com> writes:
On 5/15/17 8:14 PM, Stanislav Blinov wrote:
 On Monday, 15 May 2017 at 20:55:35 UTC, Steven Schveighoffer wrote:
 On 5/15/17 4:24 PM, Stanislav Blinov wrote:
 On Monday, 15 May 2017 at 19:44:11 UTC, Steven Schveighoffer wrote:
 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.
It "knows" it evaluated false. It doesn't know how to give user a digestible hint to make that false go away.
I'm going to snip away pretty much everything else and focus on this. The compiler absolutely 100% knows, and can demonstrate, exactly why a template constraint failed. We don't have to go any further, or make suggestions about how to fix it.
In complex constraints, that is not enough. When we have loops (i.e. over arguments, or over struct members), would it report the iteration/name?
Yes.
 Would it know to report it if `false` came several
 levels deep in the loop body?
Yes.
 Would it know that we actually *care*
 about that information? (*cough* C++ *cough* pages and pages of error
 text because of a typo...)
The idea is to give a preliminary rough report of which part of a boolean expression caused it to evaluate to false. Then you recompile with a flag to tell the compiler to do a deeper dive.
 When we have nested static ifs, it's important to see, at a glance,
 which parts of the combination were false. Again, if they're several &&,
 || in a row, or nested, pointing to a single one wouldn't in any way be
 informative.
Why not?
 When we have tests using dummy lambdas, are we to expect users to
 immediately extract the lambda body, parse it, and figure out what's wrong?
This is what you have to do today. The task has already been tried by the compiler, and the result is known by the compiler. Just have the compiler tell you.
 Just output what exactly is wrong, even if you have to recurse into
 the depths of some obscure template isXXX, and all it's recursively
 called templates, I can get the correct determination of where either
 my type isn't right, or the constraint isn't right.
Please look over my isMovable example (I'm not sure if you caught it, I posted it as a follow up to my other reply). Suppose the `false` is pointed at by the compiler:
    else static if (is(T == struct) &&
            (hasElaborateDestructor!T || hasElaborateCopyConstructor!T)) {
        foreach (m; T.init.tupleof) {
            static if (!isMovable!(typeof(m)) && (m == m.init)) {
                return false;
                       ^
                       |
            }
        }
        return true;
    } else
That is very, *very* uninformative. I don't know which member it was, I don't know which part of the conditional was false. I don't know which part of the conditional further up was true. Would the compiler know to tell me all that? Would it know to test further, to collect *all* information, so that I don't have to incrementally recompile fixing one thing at a time?
The compiler, and by extension your hand-written error checking, cannot know the true intention of the user. All it knows is you tried to do something that isn't supported. You have to figure out what is wrong and fix it. If that takes several iterations, that's what it takes. There is no solution that will give you all the answers. In your example, the compiler would point at isMovable!S as the issue. Not super-informative, but is all it gives to prevent huge outputs. Then you tell it to print more information, and it would say that false was returned when the m member of type T is being checked, at which point you could get a stack trace of what values were at each level of recursion. Everywhere a boolean evaluated to true in order to get to the point where false is returned would be colored green, every time it was false, it would be colored red, and every time a short circuit happened, it wouldn't be colored. For checking to see that "something compiles", it could identify the code that fails to compile. Again, the compiler has all this information. This isn't any harder than debugging, in fact it should be easier, as all the compiler metadata is available in memory, and most of the information can be conveyed without having to poke around. And it should be just about as good as your hand-written message. But always accurate, and instantly available to all existing constraints.
 Most importantly, as a user who sees this for the first time, I'd have
 no idea *why* those checks are there. I'd have no context, no grounds to
 base my reasoning on, so I'd either have to jump back to docs to see if
 I missed a corner case, or start spelunking code that I didn't write,
 which is always so fun... Thing is, the compiler is exactly in that
 position. It doesn't read the docs, ever :) It's always spelunking code
 written by someone else. It can't tell what the constraint, as a unit,
 is *actually* testing for. It doesn't care that we shouldn't
 destructively move structs with const members. So it wouldn't be able to
 tell me either. All it will do is report me that that false was returned
 on that line, and (hopefully), some additional info, like member type
 and name.
We can't hand-hold everyone. At some point you have to learn programming and debugging :) -Steve
May 16 2017
parent reply Stanislav Blinov <stanislav.blinov gmail.com> writes:
On Tuesday, 16 May 2017 at 12:27:30 UTC, Steven Schveighoffer 
wrote:

 When we have tests using dummy lambdas, are we to expect users 
 to
 immediately extract the lambda body, parse it, and figure out 
 what's wrong?
This is what you have to do today. The task has already been tried by the compiler, and the result is known by the compiler. Just have the compiler tell you.
:) The compiler does not know what I'm checking for with that lambda. As far as the compiler is concerned, I'm interested in whether it compiles or not. It doesn't care what that means in the context of my constraint. Neither should the user.
 The compiler, and by extension your hand-written error 
 checking, cannot know the true intention of the user.
The constraint *is* hand-written error checking. What I'm talking about is hand-written human-readable error messages :) It's not about knowing user intentions, it's about informing them why they made a mistake.
 All it knows is you tried to do something that isn't supported. 
 You have to figure out what is wrong and fix it. If that takes 
 several iterations, that's what it takes. There is no solution 
 that will give you all the answers.
Hold on a second, I think there's a mixup in terminology here. A user (caller) of move() is not supposed to be interested in what particular evaluation chain caused the constraint to be false. I, as an author, declare a contract (constraint). I am not interested in user's intentions. I am interested that I'm being called correctly. When the user violates the contract, I must inform them they did so, by reporting *why* their argument does not satisfy me. Not by telling *how* I figured that out (compiler output), but by telling *what* is wrong with the argument (human-readable error message). If the user does want to know how the constraint caught their mistake, they're free to inspect what the compiler outputs. That is why (static) asserts have an optional string argument: to display a clean an readable error message, instead of just dumping some code on the user. The same thing, in my opinion, is needed for constraints.
 In your example, the compiler would point at isMovable!S as the 
 issue. Not super-informative, but is all it gives to prevent 
 huge outputs. Then you tell it to print more information, and 
 it would say that false was returned when the m member of type 
 T is being checked, at which point you could get a stack trace 
 of what values were at each level of recursion. Everywhere a 
 boolean evaluated to true in order to get to the point where 
 false is returned would be colored green, every time it was 
 false, it would be colored red, and every time a short circuit 
 happened, it wouldn't be colored.
Or the user could just read a string "This overload cannot be called, because argument 1 (struct S) has a destructor and non-statically initialized const members". No "then", no printing more information, no stack traces. User is informed their type is wrong and *why* it is wrong. If they disagree, if they think there's a bug in the constraint, or if they're interested in how the constraint works, they're free to go through all the hoops you describe.
 We can't hand-hold everyone. At some point you have to learn 
 programming and debugging :)
It's not about hand-holding. If I call a function from a foreign API, and the call does not compile, I'd want to know why it didn't compile, not what the author of the API did to make it not compile. I'd want to know what *I* must do to make it compile. Preferably without dissecting the API in order to find that out.
May 16 2017
parent reply Steven Schveighoffer <schveiguy yahoo.com> writes:
On 5/16/17 9:54 AM, Stanislav Blinov wrote:
 On Tuesday, 16 May 2017 at 12:27:30 UTC, Steven Schveighoffer wrote:

 When we have tests using dummy lambdas, are we to expect users to
 immediately extract the lambda body, parse it, and figure out what's
 wrong?
This is what you have to do today. The task has already been tried by the compiler, and the result is known by the compiler. Just have the compiler tell you.
:) The compiler does not know what I'm checking for with that lambda. As far as the compiler is concerned, I'm interested in whether it compiles or not. It doesn't care what that means in the context of my constraint. Neither should the user.
You seem to be not understanding that a hand-written message of "needs to have member x" or something conveys the same information as the compiler saying "Error in this line: auto test = T.x" Sure, it's not proper English. It gets the job done, and I don't have to instrument all my functions and template constraint helpers. I don't need to explode compile times, or debug why my messages don't match what actually happens.
 The compiler, and by extension your hand-written error checking,
 cannot know the true intention of the user.
The constraint *is* hand-written error checking. What I'm talking about is hand-written human-readable error messages :) It's not about knowing user intentions, it's about informing them why they made a mistake.
hand-written == hand-compiled. That is, I translated what I think this boolean condition means into a human-readable format by hand. I could have got it wrong. Then a nice message is useless.
 All it knows is you tried to do something that isn't supported. You
 have to figure out what is wrong and fix it. If that takes several
 iterations, that's what it takes. There is no solution that will give
 you all the answers.
Hold on a second, I think there's a mixup in terminology here. A user (caller) of move() is not supposed to be interested in what particular evaluation chain caused the constraint to be false. I, as an author, declare a contract (constraint). I am not interested in user's intentions. I am interested that I'm being called correctly. When the user violates the contract, I must inform them they did so, by reporting *why* their argument does not satisfy me. Not by telling *how* I figured that out (compiler output), but by telling *what* is wrong with the argument (human-readable error message).
The constraint tells the user why it doesn't work. There is no extra effort required. It's human readable (I know what isInputRange!R means). I don't need a specialized full-sentence message to understand that.
 If the user does want to know how the constraint caught their mistake,
 they're free to inspect what the compiler outputs.
And here is the problem. Your solution doesn't get us any closer to that. It's actually quite painful to do this today. When I have a type like this: struct S { int foo; } and the hand-written error message says: "Your type must be a struct that contains an integer property named 'foo'". How does that help me figure out what I did wrong? We can spend all day arguing over how nice this message is, but the truth is, what the constraint writer put in the constraints, and how the compiler is interpreting it, may be 2 different things. This helps nobody.
 That is why (static) asserts have an optional string argument: to
 display a clean an readable error message, instead of just dumping some
 code on the user. The same thing, in my opinion, is needed for constraints.
If assert(x == 5) would just print "Error: x == 4" automatically, we could eliminate most needs for a message. You could solve this with a message, but again, this is a huge task to undertake on ALL code in existence, rather than fixing it all to a "good enough" degree that we don't need the messages. And it avoids the "human compiler" that is prone to error.
 In your example, the compiler would point at isMovable!S as the issue.
 Not super-informative, but is all it gives to prevent huge outputs.
 Then you tell it to print more information, and it would say that
 false was returned when the m member of type T is being checked, at
 which point you could get a stack trace of what values were at each
 level of recursion. Everywhere a boolean evaluated to true in order to
 get to the point where false is returned would be colored green, every
 time it was false, it would be colored red, and every time a short
 circuit happened, it wouldn't be colored.
Or the user could just read a string "This overload cannot be called, because argument 1 (struct S) has a destructor and non-statically initialized const members". No "then", no printing more information, no stack traces. User is informed their type is wrong and *why* it is wrong. If they disagree, if they think there's a bug in the constraint, or if they're interested in how the constraint works, they're free to go through all the hoops you describe.
That doesn't help me if the constraint author didn't put that message, or the constraint author put the *wrong* message, or wrote the constraint incorrectly. -Steve
May 16 2017
parent reply Stanislav Blinov <stanislav.blinov gmail.com> writes:
On Tuesday, 16 May 2017 at 15:47:37 UTC, Steven Schveighoffer 
wrote:
 On 5/16/17 9:54 AM, Stanislav Blinov wrote:
 On Tuesday, 16 May 2017 at 12:27:30 UTC, Steven Schveighoffer 
 wrote:

 When we have tests using dummy lambdas, are we to expect 
 users to
 immediately extract the lambda body, parse it, and figure 
 out what's
 wrong?
This is what you have to do today. The task has already been tried by the compiler, and the result is known by the compiler. Just have the compiler tell you.
:) The compiler does not know what I'm checking for with that lambda. As far as the compiler is concerned, I'm interested in whether it compiles or not. It doesn't care what that means in the context of my constraint. Neither should the user.
You seem to be not understanding that a hand-written message of "needs to have member x" or something conveys the same information as the compiler saying "Error in this line: auto test = T.x"
You are nitpicking again, or, at least, oversimplifying. I am understanding that perfectly. But we've both shown examples of constraints that are much more complex than just checking for one member. As in, the type needs to have these members and/or these properties. All of them. Or some of them. Or a set of combinations of them. With those specific traits, etc. It's not *just* "error in this line". It's "error in this line on this execution path with these conditions". And nowhere in sight is *what* all those checks are doing as a unit. Not individually, but in complex, all of them, in concert. A fresh user of your code cannot know at a glance what all of them are doing, nor should they.
 Sure, it's not proper English. It gets the job done, and I 
 don't have to instrument all my functions and template 
 constraint helpers. I don't need to explode compile times, or 
 debug why my messages don't match what actually happens.
If your messages don't match what actually happens, there's a bug in your constraint, simple as that. Which you should catch with tests.
 The compiler, and by extension your hand-written error 
 checking,
 cannot know the true intention of the user.
 hand-written == hand-compiled. That is, I translated what I 
 think this boolean condition means into a human-readable format 
 by hand. I could have got it wrong. Then a nice message is 
 useless.
Eh? You *wrote* the constraint. You *know* what all of those tests mean. You didn't translate what you *think* the condition means. You wrote the condition, you *know* what it means. If you don't, you shouldn't be writing this constraint. The only way for you to "get it wrong" is to write a bug in the constraint. Or in the compiler. You know that if you've just did is(typeof((T t) safe {})) and it returned false, T's destructor cannot be called in safe code. It's obvious, isn't it? Every D user knows that for sure. There are obscure checks, that's the way the language works. There are cases when those obscure checks need to be cleanly explained. That is *all* that I'm proposing.
 When the
 user violates the contract, I must inform them they did so, by 
 reporting
 *why* their argument does not satisfy me. Not by telling *how* 
 I figured
 that out (compiler output), but by telling *what* is wrong 
 with the
 argument (human-readable error message).
The constraint tells the user why it doesn't work. There is no extra effort required. It's human readable (I know what isInputRange!R means). I don't need a specialized full-sentence message to understand that.
*You* know what isInputRange means. This doesn't mean that all users do. And do you know, beforehand, what all possible constraints in all possible libraries that you may use in the future mean, without looking at their code? No. No one does.
 If the user does want to know how the constraint caught their 
 mistake,
 they're free to inspect what the compiler outputs.
 And here is the problem. Your solution doesn't get us any 
 closer to that. It's actually quite painful to do this today.
Of course it is painful, that's the whole point of my proposal.
 When I have a type like this:

 struct S
 {
    int foo;
 }

 and the hand-written error message says: "Your type must be a 
 struct that contains an integer property named 'foo'". How does 
 that help me figure out what I did wrong? We can spend all day 
 arguing over how nice this message is, but the truth is, what 
 the constraint writer put in the constraints, and how the 
 compiler is interpreting it, may be 2 different things. This 
 helps nobody.
It cannot be that way. Whoever wrote the constraint possesses knowledge about it's semantics. If they put wrong message there, then that's a bug, or they're a... well, not a very good person.
 That is why (static) asserts have an optional string argument: 
 to
 display a clean an readable error message, instead of just 
 dumping some
 code on the user. The same thing, in my opinion, is needed for 
 constraints.
If assert(x == 5) would just print "Error: x == 4" automatically, we could eliminate most needs for a message.
You're taking overly simplified pieces of code and base your reasoning on them, while you know full well it's almost never this simple. assert(x.veryBadlyNamedFunction == obscureStateObtainedFromElsewhere); "Error: x.veryBadlyNamedFunction == 42" Very helpful message.
 You could solve this with a message, but again, this is a huge 
 task to undertake on ALL code in existence, rather than fixing
I seem to be expressing myself poorly, as I find I must repeat it the second time already: I never suggested to change any existing code. I suggested an *optional* mechanism of reporting errors.
 it all to a "good enough" degree that we don't need the 
 messages. And it avoids the "human compiler" that is prone to 
 error.
This again. No it does not. If there's a bug in the constraint, there is a bug in the constraint. Messages or no, it's all the same. Either it does what it's supposed to do, or it doesn't. Reporting the wrong message is a bug, as is returning false for compliant type, or true for malicious one.
 stack traces. User is informed their type is wrong and *why* 
 it is
 wrong. If they disagree, if they think there's a bug in the 
 constraint,
 or if they're interested in how the constraint works, they're 
 free to go
 through all the hoops you describe.
That doesn't help me if the constraint author didn't put that message, or the constraint author put the *wrong* message, or wrote the constraint incorrectly.
And again. If they wrote the constraint incorrectly, it's their bug. If they didn't put any message, compiler still should provide it's own diagnostics (better than today ones for sure).
May 16 2017
parent Steven Schveighoffer <schveiguy yahoo.com> writes:
On 5/16/17 2:29 PM, Stanislav Blinov wrote:
 On Tuesday, 16 May 2017 at 15:47:37 UTC, Steven Schveighoffer wrote:
 On 5/16/17 9:54 AM, Stanislav Blinov wrote:
 On Tuesday, 16 May 2017 at 12:27:30 UTC, Steven Schveighoffer wrote:
 When I have a type like this:

 struct S
 {
    int foo;
 }

 and the hand-written error message says: "Your type must be a struct
 that contains an integer property named 'foo'". How does that help me
 figure out what I did wrong? We can spend all day arguing over how
 nice this message is, but the truth is, what the constraint writer put
 in the constraints, and how the compiler is interpreting it, may be 2
 different things. This helps nobody.
It cannot be that way. Whoever wrote the constraint possesses knowledge about it's semantics. If they put wrong message there, then that's a bug, or they're a... well, not a very good person.
This is the only time I have issues. Most of the time, the constraint is straightforwardly simple and the problem is obvious just by looking at the constraint and the type. When that's not the case, figuring out why the constraint doesn't seem to be doing what it says it's doing is the hard part. This can be achieved any number of ways. The most straightforward and effective way is to have the compiler simply expose what it's doing.
 That is why (static) asserts have an optional string argument: to
 display a clean an readable error message, instead of just dumping some
 code on the user. The same thing, in my opinion, is needed for
 constraints.
If assert(x == 5) would just print "Error: x == 4" automatically, we could eliminate most needs for a message.
You're taking overly simplified pieces of code and base your reasoning on them, while you know full well it's almost never this simple. assert(x.veryBadlyNamedFunction == obscureStateObtainedFromElsewhere); "Error: x.veryBadlyNamedFunction == 42" Very helpful message.
Not here to debate this "feature", but if it says what the return value was, and the name of the symbol it's checking against (and its value), then yes, combined with the usual stack trace it's incredibly useful. Most of the time, that's what I would normally write in the message, but that in itself isn't easy to do.
 You could solve this with a message, but again, this is a huge task to
 undertake on ALL code in existence, rather than fixing
I seem to be expressing myself poorly, as I find I must repeat it the second time already: I never suggested to change any existing code. I suggested an *optional* mechanism of reporting errors.
No, I mean having the optional mechanism fixes nothing. Someone then has to go through all existing code and determine what the existing constraints are testing for, and then determine the best way to write the messages. That's not a trivial amount of work.
 it all to a "good enough" degree that we don't need the messages. And
 it avoids the "human compiler" that is prone to error.
This again. No it does not. If there's a bug in the constraint, there is a bug in the constraint. Messages or no, it's all the same. Either it does what it's supposed to do, or it doesn't. Reporting the wrong message is a bug, as is returning false for compliant type, or true for malicious one.
There could be no bug in the constraint, but the message is misleading, wrong, or easy to misinterpret. It's adding another layer of potential issues. -Steve
May 16 2017
prev sibling parent Stanislav Blinov <stanislav.blinov gmail.com> writes:
On Monday, 15 May 2017 at 15:30:38 UTC, Steven Schveighoffer 
wrote:

 Imagine also a constraint like isInputRange!R. This basically 
 attempts to compile a dummy lambda. How would one handle this 
 in user-code?
Let's look at something more practical than my initial example, even if less involved than isInputRange. In a discussion not long ago it was brought up that a destructive variant of move() violates const, and this was the response: http://forum.dlang.org/post/odlv7q$16dr$1 digitalmars.com So let me try implementing the "should fail in all cases" bit.
enum bool isMovable(T) = {
    import std.traits;
    static if (!isMutable!T)
        return false;
    static if (is(T == struct) &&
            (hasElaborateDestructor!T || 
 hasElaborateCopyConstructor!T)) {
        foreach (m; T.init.tupleof) {
            static if (!isMovable!(typeof(m)) && (m == m.init)) {
                return false;
            }
        }
        return true;
    } else
        return true;
}();
Not exactly a one-liner. There are several checks to be made: - if the type itself is const/immutable, it can't be moved (can't write to const). - if it's a struct, each member has to be checked. We can't do this check with isAssignable, since assignment might be redefined. - if a member is const/immutable, we should check it's init value: if it differs from default, no constructor can change it, so it's "safe" to move destructively (provided a more involved implementation of move() than it is at the moment). - maybe one or two cases that I forgot Note that the compiler doesn't deal in the logic listed above. It deals in statements and expressions written inside that lambda. But as I'm writing it, I already possess all the information required to convey the description of failure in a human-readable manner. What I don't have is the means to present that information. And now:
T move(T)(ref T src) if (isMovable!T) { /*...*/ }
void move(T)(ref T src, ref T dst) if (isMovable!T) { /*...*/ }

struct S {
    const int value; // no default initialization, we initialize 
 in ctor
    this(int v) { value = v; }
    ~this() {}  // "elaborate" dtor, we should only move this 
 type destructively
}

S a;
auto b = move(a);
All I realistically would get from the compiler is that it can't resolve the overload, and, perhaps with some improvement in the compiler, the line in the lambda that returned false, no more. Something along the lines of:
            static if (!isMovable!(typeof(m)) && (m == m.init)) {
                return false;
                       ^
                       |
Even though this looks a tiny bit better than what we have now, it actually isn't. A user will have to look at the code of that lambda and parse it mentally in order to understand what went wrong. Whereas I could simply include actual descriptive text like "S cannot be moved because it has a destructor and uninitialized const members."
May 15 2017
prev sibling next sibling parent MysticZach <reachzach ggmail.com> writes:
On Saturday, 13 May 2017 at 14:41:50 UTC, Stanislav Blinov wrote:
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))
Ya know, even a simple new line before "candidates are:" would improve this error message! https://issues.dlang.org/show_bug.cgi?id=17400 I know it seems trivial, but attention to detail does make a difference, so I decided to make an issue for it anyway... :-/
May 15 2017
prev sibling parent reply Nick Treleaven <nick geany.org> writes:
On Saturday, 13 May 2017 at 14:41:50 UTC, Stanislav Blinov wrote:
template types(args...) {
    static if (args.length)
        alias types = AliasSeq!(typeof(args[0]), 
 types!(args[1..$]));
    else
        alias types = AliasSeq!();
}
...
    foreach(i, T; types!args) {
typeof(args) ;-)
        static if (is(T == string)) {
            pragma(msg, format!"Argument %d is a string, which 
 is not supported"
                    (i+1));
The problem with this approach is all the work required to convert existing code to use this style. Breaking the binary ops of the constraint into parts and reporting which failed (as the other replies mention) already would work with existing code, even if your approach can allow better messages. The binary ops part would immediately bring a huge improvement, despite any deficiencies.
 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.
I think we should allow inline constraints*, non-inline constraints can still be used/combined. Inline constraints are easier to read and relate to what they affect, allowing any non-inline constraint to be considered as something with a wider scope (i.e., multiple arguments). * template foo(R if isInputRange) A side benefit is enum/alias/variable templates, they don't currently allow constraints in the grammar - they could have inline constraints without harming readability (presumably why constraints aren't supported).
May 16 2017
parent reply Stanislav Blinov <stanislav.blinov gmail.com> writes:
On Tuesday, 16 May 2017 at 09:04:32 UTC, Nick Treleaven wrote:

 ...
    foreach(i, T; types!args) {
typeof(args) ;-)
Thanks :)
        static if (is(T == string)) {
            pragma(msg, format!"Argument %d is a string, which 
 is not supported"
                    (i+1));
The problem with this approach is all the work required to convert existing code to use this style. Breaking the binary ops of the constraint into parts and reporting which failed (as the other replies mention) already would work with existing code, even if your approach can allow better messages. The binary ops part would immediately bring a huge improvement, despite any deficiencies.
That's not a problem. In cases where compiler-provided diagnostic is sufficient, nothing will need to be done.
 I think we should allow inline constraints*, non-inline 
 constraints can still be used/combined. Inline constraints are 
 easier to read and relate to what they affect, allowing any 
 non-inline constraint to be considered as something with a 
 wider scope (i.e., multiple arguments).
No matter if it's inline or trailing, it needs way, *way* better reporting than what we have now, so if you were to prioritize, which one would you solve first? :)
May 16 2017
parent reply Nick Treleaven <nick geany.org> writes:
On Tuesday, 16 May 2017 at 11:20:57 UTC, Stanislav Blinov wrote:
 On Tuesday, 16 May 2017 at 09:04:32 UTC, Nick Treleaven wrote:
 The problem with this approach is all the work required to 
 convert existing code to use this style.
...
 That's not a problem. In cases where compiler-provided 
 diagnostic is sufficient, nothing will need to be done.
I don't understand. I thought you were proposing a new language feature (constraint expressions or pragma(overloadError))?
 I think we should allow inline constraints*, non-inline 
 constraints can still be used/combined. Inline constraints are 
 easier to read and relate to what they affect, allowing any 
 non-inline constraint to be considered as something with a 
 wider scope (i.e., multiple arguments).
No matter if it's inline or trailing, it needs way, *way* better reporting than what we have now, so if you were to prioritize, which one would you solve first? :)
My priority is (1) For the compiler to show which part of a constraint failed. Having inline constraints is lower priority, except: * If it turns out to be hard to implement (1) then inline constraints would be easy to implement and at least allows finer grained error messages, but not for existing code - although perhaps it's possible for dfix to auto-update certain constraints in existing code. * Inline constraints can probably be implemented in parallel by someone less able to do (1).
May 16 2017
parent Stanislav Blinov <stanislav.blinov gmail.com> writes:
On Tuesday, 16 May 2017 at 14:00:51 UTC, Nick Treleaven wrote:
 On Tuesday, 16 May 2017 at 11:20:57 UTC, Stanislav Blinov wrote:
 On Tuesday, 16 May 2017 at 09:04:32 UTC, Nick Treleaven wrote:
 The problem with this approach is all the work required to 
 convert existing code to use this style.
...
 That's not a problem. In cases where compiler-provided 
 diagnostic is sufficient, nothing will need to be done.
I don't understand. I thought you were proposing a new language feature (constraint expressions or pragma(overloadError))?
Oh. What I am proposing is add the possibility for the constraints to return a string in addition to the boolean. I am *not* proposing to remove the existing boolean checking. The compiler knows what type the constraint returns. If it's a boolean, nothing is changed. If it's a tuple, it could use the second value as a message (when it needs to output a message, i.e. when all overloads failed). pragma was just presented as an alternative. That's it :) Existing code should not break.
 My priority is (1) For the compiler to show which part of a 
 constraint failed. Having inline constraints is lower priority, 
 except:
From a user perspective, I would be interested in what the constraint failure *means*, not which part of it returned false. The constraint is there to prevent me from calling the function with the wrong types of arguments. The author decided which types are wrong. How the author checks for that is their business. Simply telling me I'm wrong does not help me stop being wrong. Showing me the "evidence" (constraint code) does not help me either, because the evidence is arbitrary, and in this case is a process, not statement of fact. Steven argues that the compiler should be able to show the cause of failure. My counter-argument is that it's not helpful, because an isolated piece of the constraint does not necessarily reflect it's intent. Diagnostic generated from arbitrary code written by the constraint author will be even more confusing than telling nothing at all. Because it is code not written by the user, it may use names not provided by the user, etc. If I wrote a call foo(x), and then compiler tells me: 'property bar.front' is not defined I'm going to take offense. I didn't pass any 'bar' to that function. Yet it's there because whoever wrote the constraint used a lambda there to test for something. Now I'd have to look at that, figure out what that 'bar' is, etc... Why should I? Why can't the constraint cleanly report what it's "false" result actually means?
May 16 2017