www.digitalmars.com         C & C++   DMDScript  

digitalmars.D.announce - Error message formatter for range primitives

reply Robert Schadek <rburners gmail.com> writes:
In 
https://forum.dlang.org/post/tfdycnibnxyryizeckjp forum.dlang.org 
I complained
that error message related to range primitives like isInputRange, 
especially on
template constraints, are not great.

As talk is cheap, and you put your code where your mouth is, I 
created

https://github.com/burner/range_primitives_helper

which you can know add to your project in version in v1.0.0 by

```sh
dub add range_primitives_helper
```




Range primitives like `isInputRange` are used in many places in D 
code.
When the usage of those primitives leads to a compile error, 
because e.g. the
passed type is not an InputRange, the error messages are often 
not very helpful.
This is especially true, if range primitives are used as function 
constraints
for function overloading.

For example:
```dlang
void fun(T)(T t) if(isInputRange!T && !isRandomAccessRange!T) {
}

void fun(T)(T t) if(isRandomAccessRange!T) {
}
```

This is at least annoying, and avoidable at best.
This library **Range Primitives Helper** helps making this less 
annoying.



```dlang
import range_primitives_helper;

enum string result = isInputRangeErrorFormatter!(T);
```

If the passed type `T` is an InputRange the `enum string result` 
will read

```dlang
T.stringof ~ " is an InputRange"
```

if `T` is not an InputRange the string will list which criteria 
of the
InputRange concept is not fulfilled by `T`;

But this is only half the work.
The other part is a bit of a refactoring effort.
Instead of having to template functions that use function 
constraints to do the
overload resolution, a better approach is to have what I would 
call a
*dispatch function* like this.

```dlang
import range_primitives_helper;

void fun(T)(T t) {
	static if(isRandomAccessRange!T) {
		funRAR(t);
	} else static if(isInputRange!T) {
		funIR(t);
	} else {
		static assert(false, "'fun' expected 'T' = "
			~ T.stringof ~ " either to be
			~ an InputRange or"
			~ " a RandomAccessRange but\n"
			~ isInputRangeErrorFormatter!(T)
			~ "\n"
			~ isRandomAccessRangeErrorFormatter!(T));
	}
}

private void funIR(T)(T t) {
}

private void funRAR(T)(T t) {
}
```

Calling `fun` with an `int` for example results in, IMO very 
nice, error message

```sh
SOURCE_LOCATION: Error: static assert:  "
'fun' expected 'T' = 'int' either to be an InputRange or a 
RandomAccessRange but
int is not an InputRange because:
	the property 'empty' does not exist
	and the property 'front' does not exist
	and the function 'popFront' does not exist
int is not an RandomAccessRange because
	the property 'empty' does not exist
	and the property 'front' does not exist
	and the function 'popFront' does not exist
	and the property 'save' does not exist
	and int must not be an autodecodable string but should be an 
aggregate type
	and int must allow for array indexing, aka. [] access"
```

If we call `fun` with a custom `struct` that looks like

```dlang
struct Thing {
	void popFront();
	 property int front() { return 0; }
}
```

we get the error string

```sh
SOURCE_LOCATION: Error: static assert:  "
'fun' expected 'T' = 'Thing' either to be an InputRange or a 
RandomAccessRange but
Thing is not an InputRange because:
	the property 'empty' does not exist
Thing is not an RandomAccessRange because
	the property 'empty' does not exist
	and the property 'save' does not exist
	and must allow for array indexing, aka. [] access"
```



The are primitives for:

| Type | std.range | range\_primitives\_helper |
| ---- | --------- | ----------------------- |
| InputRange | isInputRange | isInputRangeErrorFormatter |
| BidirectionalRange | isBidirectionalRange | 
isBidirectionalRangeErrorFormatter |
| ForwardRange | isForwardRange | isForwardRangeErrorFormatter |
| RandomAccessRange | isRandomAccessRange | 
isRandomAccessRangeErrorFormatter |
| OutputRange | isOutputRange | isOutputRangeErrorFormatter |
Jan 05 2022
next sibling parent reply Elronnd <elronnd elronnd.net> writes:
Cool project!

The mechanism you use is very special-purpose, in that you have 
to write a lot of specific code to get such nice output.  There's 
a trick I came up with, that I've been meaning to post about, 
which gives slightly less nice output, but requires no manual 
effort and is completely general.

It looks like this: first, write a validator function

bool testInputRange(T)() {
	static assert(is(typeof(T.init.empty)));
	static assert(is(typeof(T.init.front)));
	static assert(is(typeof(T.init.popFront)));
	return true;
}

(The return value is a dummy.  It's not strictly necessary, but 
I'm not going to bother getting rid of it here for the purposes 
of concision.)

We can then say:

void f(T)(T x) if (isInputRange!T) { ... }
enum isInputRange(T) = is(typeof(testInputRange!T));

as usual.  No surprises.  But now replace the definition with:

enum isInputRange(T) = is(typeof(testInputRange!T)) || 
testInputRange!T && false;

Obviously, testInputRange!T && false is just false, and 
is(typeof(testInputRange!T)) || false is just 
is(typeof(testInputRange!T)).  So this seems logically equivalent 
to the previous definition.  But now we get nice error messages:

struct S {}
f(S());

gives an error like this:

range_check.d(12): Error: static assert:  `is(typeof(S().empty))` 
is false
range_check.d(10):        instantiated from here: 
`testInputRange!(S)`
range_check.d(4):        instantiated from here: 
`isInputRange!(S)`

Telling us exactly what the problem is (no 'empty' function), 
with no manual effort.

---

There is one issue with this: it doesn't tell you about more than 
one problem at once.  In this case, S was also missing front and 
popFront, but the error message only mentioned empty.  One 
solution is as follows:

void StaticAssert(alias x)() if (x) {}
bool testInputRange(T)() {
	StaticAssert!(is(typeof(T.init.empty)));
	StaticAssert!(is(typeof(T.init.front)));
	StaticAssert!(is(typeof(T.init.popFront)));
	return true;
}

Now we get to hear about all the problems, but we don't get to 
know what they actually were:

range_check.d(12): Error: template instance 
`range_check.StaticAssert!false` does not match template 
declaration `StaticAssert(alias x)()`
   with `x = false`
   must satisfy the following constraint:
`       x`
range_check.d(13): Error: template instance 
`range_check.StaticAssert!false` does not match template 
declaration `StaticAssert(alias x)()`
   with `x = false`
   must satisfy the following constraint:
`       x`
range_check.d(14): Error: template instance 
`range_check.StaticAssert!false` does not match template 
declaration `StaticAssert(alias x)()`
   with `x = false`
   must satisfy the following constraint:
`       x`
range_check.d(4): Error: template instance 
`range_check.isInputRange!(S)` error instantiating
range_check.d(8): Error: template `range_check.f` cannot deduce 
function from argument types `!()(S)`
range_check.d(4):        Candidates are: `f(T)(T x)`
range_check.d(5):                        `f(T)(T x)`

Well, we get the line numbers (12, 13, 14), so we can check the 
source code, but it would be much nicer if the error message 
itself would tell us the problem.
Jan 05 2022
next sibling parent Elronnd <elronnd elronnd.net> writes:
On Wednesday, 5 January 2022 at 12:32:10 UTC, Elronnd wrote:
 There's a trick I came up with, that I've been meaning to post 
 about
There is another thing I should mention: the isInputRange I showed is not strictly equivalent to the standard version, because SFIAE. So a dispatch stage is still necessary, like with your method, if you want to support more than one instantiation.
Jan 05 2022
prev sibling parent reply Robert Schadek <rburners gmail.com> writes:
On Wednesday, 5 January 2022 at 12:32:10 UTC, Elronnd wrote:
 Cool project!

 The mechanism you use is very special-purpose, in that you have 
 to write a lot of specific code to get such nice output.  
 There's a trick I came up with, that I've been meaning to post 
 about, which gives slightly less nice output, but requires no 
 manual effort and is completely general.
I would guess with my library your manual effort, at least for range primitives, is down to ```sh dub add range_primitives_helper ``` ;-)
Jan 05 2022
parent Elronnd <elronnd elronnd.net> writes:
On Wednesday, 5 January 2022 at 13:43:19 UTC, Robert Schadek 
wrote:
 I would guess with my library your manual effort, at least for 
 range primitives, is down to

 ```sh
 dub add range_primitives_helper
 ```
Indeed. This is for when you want to write your own constraints.
Jan 05 2022
prev sibling parent WebFreak001 <d.forum webfreak.org> writes:
On Wednesday, 5 January 2022 at 09:32:36 UTC, Robert Schadek 
wrote:
 In 
 https://forum.dlang.org/post/tfdycnibnxyryizeckjp forum.dlang.org I complained
 that error message related to range primitives like 
 isInputRange, especially on
 template constraints, are not great.

 [...]
cool! As I'm not a fan of needing to refactor code I made my first DMD PR to try to make it possible to include this in phobos here: https://github.com/dlang/dmd/pull/13511 ```d source/app.d(43,5): Error: template `app.fun` cannot deduce function from argument types `!()(Sample1)` source/app.d(22,6): Candidates are: `fun(T)(T t)` with `T = Sample1` must satisfy the following constraint: ` isInputRange!T: Sample1 is not an InputRange because: the function 'popFront' does not exist` source/app.d(24,6): `fun(T)(T t)` with `T = Sample1` must satisfy the following constraint: ` isRandomAccessRange!T: Sample1 is not an RandomAccessRange because the function 'popFront' does not exist and the property 'save' does not exist and must allow for array indexing, aka. [] access` ```
Jan 11 2022