Simple and effective approaches to constraint error messages

Andrei Alexandrescu via Digitalmars-d digitalmars-d at puremagic.com
Mon Apr 25 10:52:58 PDT 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


More information about the Digitalmars-d mailing list