www.digitalmars.com         C & C++   DMDScript  

digitalmars.D - Simple and effective approaches to constraint error messages

reply Andrei Alexandrescu <SeeWebsiteForEmail erdani.org> writes:
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
next sibling parent Adam D. Ruppe <destructionator gmail.com> writes:
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!NotARange
This 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
prev sibling next sibling parent reply Daniel N <ufo orbiting.us> writes:
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
parent Andrei Alexandrescu <SeeWebsiteForEmail erdani.org> writes:
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
prev sibling next sibling parent reply Sebastiaan Koppe <mail skoppe.eu> writes:
On Monday, 25 April 2016 at 17:52:58 UTC, Andrei Alexandrescu 
wrote:
 Destroy!

 Andrei
What 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
parent Andrei Alexandrescu <SeeWebsiteForEmail erdani.org> writes:
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
prev sibling next sibling parent reply Steven Schveighoffer <schveiguy yahoo.com> writes:
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
parent reply "H. S. Teoh via Digitalmars-d" <digitalmars-d puremagic.com> writes:
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:
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.
What about displaying the full context with -v? The compiler currently already uses -v to show long error messages that are truncated by default.
 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
parent Steven Schveighoffer <schveiguy yahoo.com> writes:
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:
 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.
What about displaying the full context with -v? The compiler currently already uses -v to show long error messages that are truncated by default.
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.
 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'
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. -Steve
Apr 27 2016
prev sibling next sibling parent QAston <qaston gmail.com> writes:
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
prev sibling next sibling parent reply Marc =?UTF-8?B?U2Now7x0eg==?= <schuetzm gmx.net> writes:
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
parent Timon Gehr <timon.gehr gmx.ch> writes:
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
prev sibling next sibling parent Kagamin <spam here.lot> writes:
https://issues.dlang.org/show_bug.cgi?id=9626 ?
Apr 26 2016
prev sibling next sibling parent Atila Neves <atila.neves gmail.com> writes:
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
prev sibling parent reply Meta <jared771 gmail.com> writes:
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
parent reply Andrei Alexandrescu <SeeWebsiteForEmail erdani.org> writes:
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
parent Meta <jared771 gmail.com> writes:
On Tuesday, 26 April 2016 at 15:56:55 UTC, Andrei Alexandrescu 
wrote:
 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
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-concepts
Apr 26 2016