Now that's a DIP that could use some love

Adam D. Ruppe destructionator at gmail.com
Mon Sep 14 00:00:15 UTC 2020


On Sunday, 13 September 2020 at 22:29:00 UTC, Andrei Alexandrescu 
wrote:
> Solves a long-standing, difficult problem of outputting 
> meaningful, useful error messages.

Since this was written, the compiler's output has improved 
significantly.

---
void foo(T)() if(true && is(T == string)) {}
void foo(T)() if(false && is(T == float)) {}

void main() { foo!int(); }
---

$ dmd cons
cons.d(4): Error: template cons.foo cannot deduce function from 
argument types
(int)(), candidates are:
cons.d(1):        foo(T)()
   with T = int
   must satisfy the following constraint:
        is(T == string)
cons.d(2):        foo(T)()
   with T = int
   must satisfy the following constraint:
        false


What benefit would it bring adding a string to it? Instead of 
`is(T == string)` it would say "T must be a string"? Not really 
an improvement. Consider a case like `isInputRange!T`. Frequently 
the question is: why isn't it considered an input range? The 
answer might be "it is missing method popFront", and that would 
help, but just rephrasing "must satisfy the following constraint: 
isInputRange!T" as "T must be an input range" isn't a significant 
improvement.

The block format's ability to unroll loops and provide a more 
detailed message for individual items has potential to improve 
the status quo in niche cases, but I'm not convinced this is a 
good general solution. Should every constraint repeat the same 
strings and always have to build it themselves? It doesn't even 
seem possible to abstract the message to a library here. You 
could have a condition and a message, as two separate functions, 
but still work on the user side, every time they use it.

* * *

I think in my perfect world one of two things would happen:

1) You can use constraint functions that return a string or array 
of strings. If this string is null, it *passes*. If not, the 
returned string(s) is(are) considered the error message(s).

string checkInputRange(T)() {
      if(!hasMember!(T, "popFront"))
          return "missing popFront";
      if(!hasMember!(T, "empty"))
          return "must have empty";
      if(!hasMember!(T, "front"))
          return "must have front";
      return null; // success
}

void foo(T)() if(checkInputRange!T) {}

foo!int;

   with T = (int)
   must satisfy the following constraint:
     checkInputRange!T ("missing popFront");


This breaks backward compatibility since currently a null string 
implicitly casts to boolean false. So it is the opposite behavior 
right now. But it could perhaps be opt-in somehow.

It also addresses the loop cases of the DIP because the CTFE 
helper check function can just loop over and build the more 
detailed error messages that way.

It might be possible to do this with the DIP  but it will take 
some repetition:

void foo(T)() if(checkInputRange!T.passed, 
checkInputRange!T.error) {}

Indeed, checkInputRange might return an object that has 
opCast(T:bool)() { return this.errors == 0; }... but it still 
must be repeated to get the message out.

While that would be possible, I believe a better DIP would be to 
let the compiler recognize the pattern and work it automatically 
for us. (And btw, __traits(compiles) might even return such a 
CompileError object, with implicit cast to bool if no errors 
occurred, and make the errors available for forwarding if they 
did.)

2) Allow for `void` functions (or something) to be used in 
constraints. If it throws an exception, the constraint fails and 
the exception is used as the failing constraint error message. If 
not, it is considered to pass.

It is currently illegal to use a void function as part of a 
constraint, making it possible to ease into this without breaking 
existing code.

Currently if a constraint calls a bool returning function and it 
throws an exception, it actually does print the message! But it 
also kills the whole compile.

---
bool check(T)() {
         static if(is(T == float))
                 return true;
         else
                 throw new Exception(T.stringof ~ " is not a 
float");
}

void foo(T...)() if(check!T()) {}
void foo(T)() if(is(T == int)) {}

void main() {
         foo!float();
         foo!int();
}
---

cons.d(5): Error: uncaught CTFE exception object.Exception("int 
is not a float")
cons.d(8):        called from here: check()

Which is a pretty miserable state of affairs (that error message 
doesn't even show the template instantiation point in user code!).

If we used exceptions for this purpose, it would just mean the 
first overload fails, allowing the instance to use the second 
overload.

CTFE check functions may also catch the exception and add 
supplemental context through the usual mechanisms.


More information about the Digitalmars-d mailing list