Producing nicer template errors in D libraries

Don Clugston dac at nospam.com
Wed Apr 11 04:42:54 PDT 2012


On 10/04/12 21:45, H. S. Teoh wrote:
> A lot of template code (e.g. a big part of Phobos) use signature
> constraints, for example:
>
> 	void put(T,R)(R range, T data) if (isOutputRange!R) { ... }
>
> This is all nice and good, except that when the user accidentally calls
> .put on a non-range, you get reams and reams of compiler errors
> complaining that certain templates don't match,

> So here's take #2:
>
> 	void put(T,R)(R range, T data) if (assertIsOutputRange!R) { ... }
>
> 	template assertIsOutputRange(R)
> 	{
> 		static if (isOutputRange!R)
> 			enum assertIsOutputRange = true;
> 		else
> 			static assert(0, R.stringof ~ " is not an output range");
> 	}
>
> Now we can stick assertIsOutputRange everywhere there was a signature
> constraint before, without needing to introduce lots of boilerplate
> code.
>
> But what if there are several overloads of the same function, each with
> different signature constraints? For example:
>
> 	int func(T)(T arg) if (constraintA!T) { ... }
> 	int func(T)(T arg) if (constraintB!T) { ... }
> 	int func(T)(T arg) if (constraintC!T) { ... }
>
> If constraintA asserts, then the compiler will not compile the code even
> if the call actually matches constraintB.
>
> So for cases like this, we introduce a catchall overload:
>
> 	int func(T)(T arg)
> 		if (!constraintA!T&&  !constraintB!T&&  !constraintC!T)
> 	{
> 		static assert(0, "func can't be used for type " ~
> 			T.stringof ~ " because<insert some excuse here>");
> 	}
>
> Now the compiler will correctly resolve the template to instantiate,
> while still providing a nice error message for when nothing matches.

This is the way we used to do it, before we had template constraints.
Although it works OK in simple cases, it doesn't scale -- you need to 
know all possible template constraints.

I would like to see something in the language conceptually like:

int func(T)(T arg) else { ... }

for a template which is instantiated only if all constraints have 
failed. 'default' is another keyword which could be used, and 
'if(false)' is another, but else is probably more natural.
Any attempt to instantiate an 'else' template always results in an 
error, just as now. (in practice: if instantiating the else template 
didn't trigger a static assert, a generic error message is issued)

It is an error for there to be more than one matching 'else' template.


>
> What do y'all think of this idea?
>
> (Personally I think it's really awesome that D allows you to customize
> compiler errors using static assert, and we should be taking advantage
> of it much more. I propose doing this at strategic places in Phobos,
> esp. where you'd otherwise get errors from 5 levels deep inside some
> obscure nested template that hardly anybody understands how it's related
> to the original failing instantiation (e.g. a no-match error from
> appendArrayWithElemImpl instantiated from appendToArrayImpl instantiated
> from nativeArrayPutImpl instantiated from arrayPutImpl instantiated from
> putImpl instantiated from put -- OK I made that up, but you get the
> point).)

Definitely.

Incidentally, when all template constraints fail, the compiler could 
check them all again, and tell you exactly which conditions failed...

Algorithm: We know that:

false = !constraint1() && !constraint2() && !constraint3().

break each constraints into top-level boolean expressions.  Then 
simplify (possibly using a BDD).
easy (but common) example, if constraint1() =  !A() && B(), constraint2 
= !A() && C(), constraint3() == !A() && !B() && !D()

it simplifies to:  false = !A().
So we generate an error only saying that !A() failed.





More information about the Digitalmars-d mailing list