digitalmars.D - Simple and effective approaches to constraint error messages
- Andrei Alexandrescu (101/101) Apr 25 2016 It's been long asked in our community that failing template constraints
- Adam D. Ruppe (86/92) Apr 25 2016 This is more-or-less what I've been wanting to do (though I was
- Daniel N (9/21) Apr 25 2016 It should be possible to generate those errors even with
- Andrei Alexandrescu (3/7) Apr 25 2016 Walter said that's liable to print a rather large and unstructured pile
- Sebastiaan Koppe (17/19) Apr 25 2016 What about overloaded functions with complex constraints? How
- Andrei Alexandrescu (2/4) Apr 25 2016 Print reason for each. -- Andrei
- Steven Schveighoffer (16/18) Apr 25 2016 I like the first option. However, I think it should be deeper than that.
- H. S. Teoh via Digitalmars-d (20/39) Apr 25 2016 What about displaying the full context with -v? The compiler currently
- Steven Schveighoffer (13/43) Apr 27 2016 Sure, but the problem is that -v enables a TON of messages. I'd
- QAston (33/50) Apr 25 2016 Improvement in the generic case is a must. I personally either
- Marc =?UTF-8?B?U2Now7x0eg==?= (19/31) Apr 26 2016 I prefer this one, because it should work without modifying
- Timon Gehr (14/29) Apr 26 2016 There sometimes is short-cut evaluation. This only prints "Foo":
- Kagamin (1/1) Apr 26 2016 https://issues.dlang.org/show_bug.cgi?id=9626 ?
- Atila Neves (5/8) Apr 26 2016 I still prefer static inheritance. Having said that, I like the
- Meta (9/57) Apr 26 2016 The problem here is that *every* constraint must be modified,
- Andrei Alexandrescu (2/3) Apr 26 2016 Only if in CNF, otherwise needs changed. -- Andrei
- Meta (11/15) Apr 26 2016 True, but I bet the majority of template constraints are in this
It's been long asked in our community that failing template constraints issue better error messages. Consider: R find(R, E)(R range, E elem) { for (; !range.empty; range.popFront) if (range.front == elem) break; return range; } struct NotARange {} void main() { NotARange nar; nar = nar.find(42); } This program uses no constraints. Attempting to compile yields: /d240/f632.d(3): Error: no property 'empty' for type 'NotARange' /d240/f632.d(3): Error: no property 'popFront' for type 'NotARange' /d240/f632.d(4): Error: no property 'front' for type 'NotARange' /d240/f632.d(13): Error: template instance f632.find!(NotARange, int) error instantiating which is actually quite informative if you're okay with error messages pointing inside the template body (which is presumably a preexisting library) instead of the call site. Let's add constraints: import std.range; R find(R, E)(R range, E elem) if (isInputRange!R && is(typeof(range == elem) == bool)) { ... } ... Now we get: /d935/f781.d(16): Error: template f781.find cannot deduce function from argument types !()(NotARange, int), candidates are: /d935/f781.d(3): f781.find(R, E)(R range, E elem) if (isInputRange!R && is(typeof(range == elem) == bool)) That does not point inside the template implementation anymore (just the declaration, which is good) but is arguably more opaque: at this point it's less, not more, clear to the user what steps to take to make the code work. Even if they know what an input range is, the failing constraint is a complex expression so it's unclear which clause of the conjunction failed. ==== CNF (https://en.wikipedia.org/wiki/Conjunctive_normal_form) is a formula shape in Boolean logic that groups clauses into a top-level conjunction. The compiler could detect and use when CNF is used (as in the example above), and when printing the error message it only shows the first failing conjunction, e.g.: /d935/f781.d(16): Error: template f781.find cannot deduce function from argument types !()(NotARange, int), candidates are: /d935/f781.d(3): f781.find(R, E)(R range, E elem) constraint failed: isInputRange!NotARange This is quite a bit better - it turns out many constraints in Phobos are rather complex, so this would improve many of them. One other nice thing is no language change is necessary. ==== The basic idea here is to define pragma(err, "message") as an expression that formats "message" as an error and returns false. Then we can write: R find(R, E)(R range, E elem) if ((isInputRange!R || pragma(err, R.stringof~" must be an input range") && (is(typeof(range == elem) == bool) || pragma(err, "...")) Now, when printing the failed candidate, the compiler adds the error message(s) produced by the failing constraint. The problem with this is verbosity - e.g. we almost always want to write the same message when isInputRange fails, so naturally we'd like to encapsulate the message within isInputRange. This could go as follows. Currently: 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 })); } Envisioned (simplified): template lval(T) { static property ref T lval() { static T r = T.init; return r; } } template isInputRange(R) { enum bool isInputRange = (is(typeof({if(lval!R.empty) {}}) || pragma(err, "cannot test for empty")) && (is(typeof(lval!R.popFront()) || pragma(err, "cannot invoke popFront") (is(typeof({ return lval!R.front; })) || pragma(err, can get the front of the range)); } Then the way it goes, the compiler collects the concatenation of pragma(msg, "xxx") during the invocation of isInputRange!R and prints it if it fails as part of a constraint. Further simplifications should be possible, e.g. make is() support an error string etc. Destroy! Andrei
Apr 25 2016
On Monday, 25 April 2016 at 17:52:58 UTC, Andrei Alexandrescu wrote:/d935/f781.d(16): Error: template f781.find cannot deduce function from argument types !()(NotARange, int), candidates are: /d935/f781.d(3): f781.find(R, E)(R range, E elem) constraint failed: isInputRange!NotARangeThis is more-or-less what I've been wanting to do (though I was thinking of using color or something in the signature to show pass/fail/not tested on each clause, but your approach works too.) It is very important that it shows what failed and what the arguments are. The rest is nice, but less impactful. So this would make a big difference and should be a high priority to implement. BTW I'd also like traditional overloaded functions to show the match/not match report of arguments. It lists them now but is didn't match, int != string" or something it'd give at-a-glance info there too. But indeed, constraints are the much higher return.Let me show you what I've been toying with the last few weeks: struct Range { bool empty() { return true; } void popFront() {} int front; } // you test it at declaration point to get static errors // i recommend people do this now, even with our less-helpful // isInputRagnge mixin validateInputRange!Range; /* *************** */ import std.traits; // the validate mixin prints the errors mixin template validateInputRange(T) { static assert(isInputRange!T, checkInputRange!T); } // the is template returns bool if it passed template isInputRange(T) { enum isInputRange = checkInputRange!T.length == 0; } // and the check function generates an array of error // strings using introspection pragma(inline, true) template checkInputRange(T) { string[] checkInputRangeHelper() { string[] errors; static if(!hasMember!(T, "empty")) errors ~= "has no member `empty`"; else static if(!memberCanBeUsedInIf!(T, "empty")) errors ~= "empty cannot be used in if"; static if(!hasMember!(T, "popFront")) errors ~= "has no member `popFront`"; else static if(!isCallableWithZeroArguments!(T, "popFront")) errors ~= "`popFront()` is not callable. Found type: " ~ typeof(__traits(getMember, T, "popFront")).stringof ~ ". Expected: void()"; static if(!hasMember!(T, "front")) errors ~= "has no member `front`"; return errors; } enum checkInputRange = checkInputRangeHelper(); } /* *************** */ // these can be added to std.traits template memberCanBeUsedInIf(T, string member) { static if(__traits(compiles, (inout int = 0){ T t = T.init; if(__traits(getMember, t, member)) {} })) enum memberCanBeUsedInIf = true; else enum memberCanBeUsedInIf = false; } template isCallableWithZeroArguments(T, string member) { static if(__traits(compiles, (inout int = 0){ T t = T.init; (__traits(getMember, t, member))(); })) enum isCallableWithZeroArguments = true; else enum isCallableWithZeroArguments = false; } =============== With appropriate library support, those check functions could be pretty easily written and the rest generated automatically. Now, the compiler doesn't know anything about the error strings, but generating them with simple CTFE gives us the full language to define everything. The compiler could just learn the pattern (or we add some pragma) that when isInputRange fails, it prints out the report the library generated. But this is doable today and shouldn't break any code.
Apr 25 2016
On Monday, 25 April 2016 at 17:52:58 UTC, Andrei Alexandrescu wrote:It's been long asked in our community that failing template constraints issue better error messages. Consider: This program uses no constraints. Attempting to compile yields: /d240/f632.d(3): Error: no property 'empty' for type 'NotARange' /d240/f632.d(3): Error: no property 'popFront' for type 'NotARange' /d240/f632.d(4): Error: no property 'front' for type 'NotARange' /d240/f632.d(13): Error: template instance f632.find!(NotARange, int) error instantiating which is actually quite informative if you're okay with error messages pointing inside the template body (which is presumably a preexisting library) instead of the call site.It should be possible to generate those errors even with constraints and no library update. Currently when the compiler is in "__traits(compiles" or "is(typeof" mode, it simply gags all errors, if it instead would save them to a side buffer. Later the entire side-buffer could be dumped after a template constraint totally failed. If a constraint succeeds the buffer is cleared.
Apr 25 2016
On 04/25/2016 02:17 PM, Daniel N wrote:Currently when the compiler is in "__traits(compiles" or "is(typeof" mode, it simply gags all errors, if it instead would save them to a side buffer. Later the entire side-buffer could be dumped after a template constraint totally failed. If a constraint succeeds the buffer is cleared.Walter said that's liable to print a rather large and unstructured pile of messages. -- Andrei
Apr 25 2016
On Monday, 25 April 2016 at 17:52:58 UTC, Andrei Alexandrescu wrote:Destroy! AndreiWhat about overloaded functions with complex constraints? How would the errors look when none of the overloaded constraints fully match? auto fun(T)(T t) if (hasWheels!T && canFly!T) {} auto fun(T)(T t) if (canFloat!T && isAirtight!T) {} struct A { // suppose it has wheels and floats, but can't fly nor is it airtight } void main() { A a; a.fun(); // `Error: either make A fly or airtight` }
Apr 25 2016
On 04/25/2016 04:50 PM, Sebastiaan Koppe wrote:What about overloaded functions with complex constraints? How would the errors look when none of the overloaded constraints fully match?Print reason for each. -- Andrei
Apr 25 2016
On 4/25/16 1:52 PM, Andrei Alexandrescu wrote:It's been long asked in our community that failing template constraints issue better error messages. Consider:I like the first option. However, I think it should be deeper than that. Sometimes you have code that you are sure matches one of the constraints (e.g. isInputRange), but for some reason it doesn't. Sure, it's good to know that your struct that looks exactly like an input range isn't an input range, but to know why would be better. I realize that more context for an error may be too verbose, but an option to have the compiler tell you exactly why it is stopping compilation is good when you can't figure out the obvious reason. So for instance, having it say "constraint failed: isInputRange!NotARange" is good, but if you pass some parameter it says something like: "constraint failed: isInputRange!NotARange, std.range.primitives:146 failed to compile: x.front or something like that. This is a previous post related to this issue: http://forum.dlang.org/post/m4nnrk$1ml5$1 digitalmars.com -Steve
Apr 25 2016
On Mon, Apr 25, 2016 at 05:20:08PM -0400, Steven Schveighoffer via Digitalmars-d wrote:On 4/25/16 1:52 PM, Andrei Alexandrescu wrote:What about displaying the full context with -v? The compiler currently already uses -v to show long error messages that are truncated by default.It's been long asked in our community that failing template constraints issue better error messages. Consider:I like the first option. However, I think it should be deeper than that. Sometimes you have code that you are sure matches one of the constraints (e.g. isInputRange), but for some reason it doesn't. Sure, it's good to know that your struct that looks exactly like an input range isn't an input range, but to know why would be better. I realize that more context for an error may be too verbose, but an option to have the compiler tell you exactly why it is stopping compilation is good when you can't figure out the obvious reason.So for instance, having it say "constraint failed: isInputRange!NotARange" is good, but if you pass some parameter it says something like: "constraint failed: isInputRange!NotARange, std.range.primitives:146 failed to compile: x.front[...] What about this: when a constraint fails, display the first (related group of) error messages related to that constraint that the compiler would have emitted if errors weren't gagged. So if isInputRange fails to instantiate for some argument, the compiler would show the first error message that resulted in template instantiation failure, e.g.: std/range.d(123): Error: no property 'front' for type 'int' It's not completely nice, in that it exposes the implementation somewhat, but it seems to be more useful when something goes wrong to see concrete code that's failing than to get a message about isInputRangeImpl!(X,Y,Z) failing to compile, and you have no idea what that's supposed to mean because it's an internal Phobos implementation detail. T -- Perhaps the most widespread illusion is that if we were in power we would behave very differently from those who now hold it---when, in truth, in order to get power we would have to become very much like them. -- Unknown
Apr 25 2016
On 4/25/16 6:14 PM, H. S. Teoh via Digitalmars-d wrote:On Mon, Apr 25, 2016 at 05:20:08PM -0400, Steven Schveighoffer via Digitalmars-d wrote:Sure, but the problem is that -v enables a TON of messages. I'd definitely be willing to deal with that if that's the only way to show it.On 4/25/16 1:52 PM, Andrei Alexandrescu wrote:What about displaying the full context with -v? The compiler currently already uses -v to show long error messages that are truncated by default.It's been long asked in our community that failing template constraints issue better error messages. Consider:I like the first option. However, I think it should be deeper than that. Sometimes you have code that you are sure matches one of the constraints (e.g. isInputRange), but for some reason it doesn't. Sure, it's good to know that your struct that looks exactly like an input range isn't an input range, but to know why would be better. I realize that more context for an error may be too verbose, but an option to have the compiler tell you exactly why it is stopping compilation is good when you can't figure out the obvious reason.Absolutely, that's what I'm looking for. One thing that I remember being an issue with isXRange (can't remember which one) is that it required is(typeof(r.front) == ElementType!R), which looks innocuous enough. However, for ranges where front is a method, but is not labeled property, this fails. This has since been fixed. Someone who makes such a range, and then sees it failing because of some obscure compiler behavior is almost certainly never going to figure this out without an in-depth investigation. Having the compiler just tell them the answer is sooo much better. -SteveSo for instance, having it say "constraint failed: isInputRange!NotARange" is good, but if you pass some parameter it says something like: "constraint failed: isInputRange!NotARange, std.range.primitives:146 failed to compile: x.front[...] What about this: when a constraint fails, display the first (related group of) error messages related to that constraint that the compiler would have emitted if errors weren't gagged. So if isInputRange fails to instantiate for some argument, the compiler would show the first error message that resulted in template instantiation failure, e.g.: std/range.d(123): Error: no property 'front' for type 'int'
Apr 27 2016
On Monday, 25 April 2016 at 17:52:58 UTC, Andrei Alexandrescu wrote:==== CNF (https://en.wikipedia.org/wiki/Conjunctive_normal_form) is a formula shape in Boolean logic that groups clauses into a top-level conjunction. The compiler could detect and use when CNF is used (as in the example above), and when printing the error message it only shows the first failing conjunction, e.g.: /d935/f781.d(16): Error: template f781.find cannot deduce function from argument types !()(NotARange, int), candidates are: /d935/f781.d(3): f781.find(R, E)(R range, E elem) constraint failed: isInputRange!NotARange This is quite a bit better - it turns out many constraints in Phobos are rather complex, so this would improve many of them. One other nice thing is no language change is necessary.Improvement in the generic case is a must. I personally either comment out the constraint in the lib source (if I can) or recreate the predicate in a context where I actually can see the error message. That's tedious and makes me hate template constraints because for templated libraries the source is there anyway, I prefer to be given real error (which shows me the exact issue) rather than a mystery puzzle. Could your proposal be extended with showing the evaluation for isInputRange!NotARange to see why it returns false for given type, i.e. to see that compiler error:Error: no property 'empty' for type 'NotARange'So for example the error message could look like: /d935/f781.d(16): Error: template f781.find cannot deduce function from argument types !()(NotARange, int), candidates are: /d935/f781.d(3): f781.find(R, E)(R range, E elem) constraint failed: isInputRange!NotARange constraint stacktrace: std.range.primitives.isInputRange!NotARange boolean expression: is(typeof( (inout int = 0) { NotARange r = NotARange.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 })); failed with error: no property 'empty' for type 'NotARange' Btw. I see you've taken a focus on making D more usable. Probably teaching D to new people gave you some very needed perspective :P. Big thanks!
Apr 25 2016
On Monday, 25 April 2016 at 17:52:58 UTC, Andrei Alexandrescu wrote:====I prefer this one, because it should work without modifying library or user code.==== The basic idea here is to define pragma(err, "message") as an expression that formats "message" as an error and returns false. Then we can write: R find(R, E)(R range, E elem) if ((isInputRange!R || pragma(err, R.stringof~" must be an input range") && (is(typeof(range == elem) == bool) || pragma(err, "..."))Currently, there is no boolean short-cut evaluation in template constraints, see: bool foo()() { pragma(msg, "foo"); return true; } bool bar()() { pragma(msg, "bar"); return true; } void main() { static assert(__traits(compiles, foo() || bar())); } Prints "foo" and "bar", even though bar() wouldn't need to be evaluated anymore after foo() returned true.
Apr 26 2016
On 26.04.2016 13:26, Marc Schütz wrote:Currently, there is no boolean short-cut evaluation in template constraints, see: bool foo()() { pragma(msg, "foo"); return true; } bool bar()() { pragma(msg, "bar"); return true; } void main() { static assert(__traits(compiles, foo() || bar())); } Prints "foo" and "bar", even though bar() wouldn't need to be evaluated anymore after foo() returned true.There sometimes is short-cut evaluation. This only prints "Foo": template Foo(){ pragma(msg, "Foo"); enum Foo=true; } auto bar()(){ pragma(msg, "bar"); return true; } void main(){ static assert(Foo!()||bar()); } I don't see the point of the different behaviour for those cases.
Apr 26 2016
On Monday, 25 April 2016 at 17:52:58 UTC, Andrei Alexandrescu wrote:It's been long asked in our community that failing template constraints issue better error messages. Consider: [...]I still prefer static inheritance. Having said that, I like the pragma(err) thing in the template constraint. Atila
Apr 26 2016
On Monday, 25 April 2016 at 17:52:58 UTC, Andrei Alexandrescu wrote:==== The basic idea here is to define pragma(err, "message") as an expression that formats "message" as an error and returns false. Then we can write: R find(R, E)(R range, E elem) if ((isInputRange!R || pragma(err, R.stringof~" must be an input range") && (is(typeof(range == elem) == bool) || pragma(err, "...")) Now, when printing the failed candidate, the compiler adds the error message(s) produced by the failing constraint. The problem with this is verbosity - e.g. we almost always want to write the same message when isInputRange fails, so naturally we'd like to encapsulate the message within isInputRange. This could go as follows. Currently: 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 })); } Envisioned (simplified): template lval(T) { static property ref T lval() { static T r = T.init; return r; } } template isInputRange(R) { enum bool isInputRange = (is(typeof({if(lval!R.empty) {}}) || pragma(err, "cannot test for empty")) && (is(typeof(lval!R.popFront()) || pragma(err, "cannot invoke popFront") (is(typeof({ return lval!R.front; })) || pragma(err, can get the front of the range)); } Then the way it goes, the compiler collects the concatenation of pragma(msg, "xxx") during the invocation of isInputRange!R and prints it if it fails as part of a constraint. Further simplifications should be possible, e.g. make is() support an error string etc.The problem here is that *every* constraint must be modified, which will a long and tedious process. The nice part about 1 is that every constraint gets it for free. Applying it recursively would provide a nice "stack trace" of constraint failure so the user can see exactly what went wrong and where. The problem then becomes the verbosity of errors, but we already have ways of dealing with that such as the -v switch.
Apr 26 2016
On 04/26/2016 11:35 AM, Meta wrote:The nice part about 1 is that every constraint gets it for free.Only if in CNF, otherwise needs changed. -- Andrei
Apr 26 2016
On Tuesday, 26 April 2016 at 15:56:55 UTC, Andrei Alexandrescu wrote:On 04/26/2016 11:35 AM, Meta wrote:True, but I bet the majority of template constraints are in this form. The wiki page you linked also says that clauses that are not in CNF can be rewritten in CNF, but doing that automatically in the compiler would probably be difficult, bug-ridden, and annoying for end users. Are you familiar with Nim's implementation of concepts? I think somebody actually implemented something close to this in D using inheritance and introspection. http://nim-lang.org/docs/manual.html#generics-conceptsThe nice part about 1 is that every constraint gets it for free.Only if in CNF, otherwise needs changed. -- Andrei
Apr 26 2016