Simple and effective approaches to constraint error messages

Adam D. Ruppe via Digitalmars-d digitalmars-d at puremagic.com
Mon Apr 25 11:09:12 PDT 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.



More information about the Digitalmars-d mailing list