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