Jump to page: 1 2
Thread overview
Simple and effective approaches to constraint error messages
Apr 25, 2016
Adam D. Ruppe
Apr 25, 2016
Daniel N
Apr 25, 2016
Sebastiaan Koppe
Apr 25, 2016
H. S. Teoh
Apr 25, 2016
QAston
Apr 26, 2016
Marc Schütz
Apr 26, 2016
Timon Gehr
Apr 26, 2016
Kagamin
Apr 26, 2016
Atila Neves
Apr 26, 2016
Meta
Apr 26, 2016
Meta
April 25, 2016
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.

Idea #1: Detect and use CNF, print which clause 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.

Idea #2: Allow custom error messages
====

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
April 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!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 hard to read a long list. If the computer just said "argument #2 didn't match, int != string" or something it'd give at-a-glance info there too.

But indeed, constraints are the much higher return.

> Idea #2: Allow custom error messages

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.

April 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.


April 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
April 25, 2016
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`
}
April 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
April 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
April 25, 2016
On Monday, 25 April 2016 at 17:52:58 UTC, Andrei Alexandrescu wrote:
> Idea #1: Detect and use CNF, print which clause 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.

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!

April 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:
> >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
April 26, 2016
On Monday, 25 April 2016 at 17:52:58 UTC, Andrei Alexandrescu wrote:
> Idea #1: Detect and use CNF, print which clause failed
> ====
>

I prefer this one, because it should work without modifying library or user code.

> Idea #2: Allow custom error messages
> ====
>
> 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.
« First   ‹ Prev
1 2