March 05, 2019
On Monday, 4 March 2019 at 16:49:25 UTC, Sebastiaan Koppe wrote:
> On Monday, 4 March 2019 at 15:47:12 UTC, H. S. Teoh wrote:
>> [...]
>
> It's even worse. There might be another sig constraint that is perfectly happy. It might even be in another library. Your function has no way to know that. Ergo, error messages are hard.
>
> With Rust a type declares to conform to a trait explicitly. Error messages are easy.
>
> With D it seems we are reinventing traits, poorly. And all because we wanted to avoid naming them?

https://github.com/atilaneves/concepts

Not the same, but works well enough for me.
March 06, 2019
On Monday, 4 March 2019 at 15:47:12 UTC, H. S. Teoh wrote:
> On Mon, Mar 04, 2019 at 02:50:11PM +0000, Adam D. Ruppe via Digitalmars-d wrote: [...]
>> How many template constraint messages would be nice if it just said "missing popFront" instead of 40 lines of overload failed spam?
> [...]
>
> This is why I'm starting to think sig constraints are not the genius idea they first appeared to be. The basic problem is that when you write a sig constraint, you're basically saying "I only accept template arguments if they don't cause an error, otherwise it's not my problem and somebody else can pick up the tab", whereas the essence of user-friendly error-reporting is "I accept everything that looks like it ought to work, and if something breaks, I'll tell you why it didn't work".
>

Personally, I think the problem comes down to this: we have no way of propagating errors from  inside template constraints to the user.

I think the way to go is instead of returning `false`, have __traits(compiles) (which is really the core source of these difficulties) return an "error object" that evaluates to false, but also encodes additional information about the problem, recursively.

Propagate this through short-circuiting - ErrorObject && bla = true, bla && ErrorObject = ErrorObject, etc.

The compiler can then pick up on the fact that an error object was the reason why a template constraint failed to give a highly informative error message.

For instance:

struct Foo
{
  void bar() { }
}

enum hasFooCall(T) = __traits(compiles, T.init.foo());

void callFoo(T)(T t)
if (hasFooCall!T)
{
  t.foo();
}

callFoo!Foo(Foo.init);

then errors with

Error: template onlineapp.callFoo cannot deduce function from argument types !()(Foo), candidates are:
  onlineapp.callFoo(T)(T t) if (hasFooCall!T)
  Failed because:
    hasFooCall(Foo): no property foo for type Foo

March 06, 2019
On Wednesday, 6 March 2019 at 12:48:08 UTC, FeepingCreature wrote:
> I think the way to go is instead of returning `false`, have __traits(compiles) (which is really the core source of these difficulties) return an "error object" that evaluates to false, but also encodes additional information about the problem, recursively.

YES!

I'd go further and say, use error objects in any trait that returns a boolean, and in is expressions.
March 07, 2019
Am Mon, 04 Mar 2019 17:18:05 -0800 schrieb H. S. Teoh:

>> ... the problem (from the perspective of issuing a nice error) is that you can arbitrarily compose logic which makes sorting the what from the chaff extremely difficult and that signal to noise is very important (ever used -verrors=spec ?). Believe me I tried, saw a suggestion by aliak in a Phobos PR thread and thought that would make things so much easier, and thus arose that DIP.
> 
> Well, if you're looking for a perfect solution, then yes this will be very complicated and hairy to implement.
> 
> But to take care of the most common case, all we have to do is to assume that sig constraints are of the form (A && B && C && ...).  The compiler only needs to report which of these top level conjuncts failed.  If a sig constraint isn't of this form, then fallback to reporting the entire constraint as failed, i.e., treat it as the case (A) (single-argument conjunction).
> 
> It's not a perfect solution, but having this is already a lot better than the current pessimal state of things.  Your DIP represents an improvement over this most basic step, but IMO we should at least have this basic step first.  Don't let the perfect become the enemy of the good yet again.
> 
> 
> T

The DIP seems kind of intrusive to me. And Parsing a DNF form has one major drawback: For quite some time now we actually advised people to refactor their constraints into external helper functions, in order to have nicer ddoc. E.g. isDigest!X instead of isOutputRange!X && ... So now, we'd have to recommend the exact opposite. And both approaches are limited.

I think we have to take a step back here and see if we can't come up with a minimal composable solution, based on D's current features. So to analyze this problem:

We have a complex template constraint system, which is a superset of
concepts. We can implement concepts in the library just fine (e.g.
https://github.com/atilaneves/concepts). What we can't do is provide nice
error messages.
But why is that? Well, all checking is done by CTFE D code, so with a
CTFE library implementation of concepts, the compiler actually knows
nothing about the concepts. So the only one being able to generate proper
error messages is this CTFE library code. We don't we do that? Because we
have no proper way to emit error messages! The pragma msg/static assert
approaches all don't compose well with other D features
(overloading, ...) and they won't produce nice error messages anyway.
Probably better, but not nice.

So what if we simply introduce a bool __traits(constraintCheck, alias, condition, formatString) hook? Now we could do this:

struct InputRange
{
    T front;
    void popFront();
}

bool implements(T, Interface)
{
    bool result = true;
    foreach (member; Interface)
    {
        if (!hasMember(T, member))
            result &= __traits(constraintCheck, T, false, "Type %S does
not implement interface member " ~ niceFormatting(...));
    }
    return result;
}

void testRange(R, T)(R range) if (implements!(R, InputRange!T))
{

}

void testRange(R, T)(R range) if (implements!(R, OutputRange!T))
{

}
====================
test.d:6:8: Error: template »testRange« cannot deduce function from
argument types »!(Foo, Bar)« candidates are:

test.d:1:6: Note: »(R, T)(R range) if (implements!(R, InputRange!T))«:
     R: Type 'Foo' does not implement interface member 'Bar
     InputRange.front'.
     R: Type 'Foo' does not implement interface member 'popFront()'.

     testRange!(Foo, Bar);
                 ^


test.d:4:6: Note: »(R, T)(R range) if (implements!(R, OutputRange!T))«:
     R: Type 'Foo' does not implement interface member 'put(Bar)'.

     testRange!(Foo, Bar);
                 ^


void testOdd(uint n)() if (__traits(constraintCheck, n, n % 2 == 1, "Need
an odd integer."))
====================
test.d:6:8: Error: template »testOdd« cannot deduce function from
argument types »!(2)« candidates are:

test.d:1:6: Note: »testOdd(uint n)«: n: Need an odd integer.
     testOdd(2);
             ^

I think this is really all we need to have to implement a replacement for concepts with perfectly nice error messages. We may have to put some thought into the exact design of __traits(constraintCheck), but I think it's perfectly possible. And if we the provide a implementsConcept!T in phobos, we should be fine. Even DDOC generators can largely benefit from this without any changes, but they could also recognize the `implementsConcept` name and provide specially formatted docs. An this approach will preserve flexibility of the current system while providing nice error messages for more cases than concepts could (see testOdd above).

Open questions:

* What to do if there are multiple overloads. Print diagnostics for all?
* If multiple __traits(constraintCheck) fail, do we print all?
* If multiple failed checks reference same alias we should merge the
  diagnostic source code location info for all these?
* Do we allow the alias to refer to a member of some parameter type? If
  so, where will diagnostics point at: the parameter or the definition of
  the member?:

test.d:1:6: Note: »(R, T)(R range) if (implements!(R, OutputRange!T))«:
     R: interface member 'Foo.put(Bar)' is not @safe.

     test.d:2.4 void put(T t)
                            ^
* What do we want to allow in the format string? I think we need at least
  the original type name in user context, though maybe we can also get
  that in some other way. Maybe the original parameter name is useful?

* We should also consider what locations to print (Is the location of the original overload definition useful?)


-- 
Johannes
March 07, 2019
Am Wed, 06 Mar 2019 12:48:08 +0000 schrieb FeepingCreature:

> Personally, I think the problem comes down to this: we have no way of propagating errors from  inside template constraints to the user.
> 
> I think the way to go is instead of returning `false`, have __traits(compiles) (which is really the core source of these difficulties) return an "error object" that evaluates to false, but also encodes additional information about the problem, recursively.
> 
> Propagate this through short-circuiting - ErrorObject && bla = true, bla && ErrorObject = ErrorObject, etc.
> 
> The compiler can then pick up on the fact that an error object was the reason why a template constraint failed to give a highly informative error message.

I see we both had the same idea! Instead of special casing traits(compiles) and making the compiler deduce an error message I'd let the CTFE library code do that, see my post about __traits(constraintCheck). Also we should allow reporting multiple errors at once, instead of one at a time (e.g. a Type not implementing front and popFront for input ranges should report both problems at once).

But something like this is in my opinion the best way to go.


-- 
Johannes
March 08, 2019
On Thursday, 7 March 2019 at 22:09:29 UTC, Johannes Pfau wrote:
> * What to do if there are multiple overloads. Print diagnostics for all?

It's not ideal, but a -constraintcheck compiler switch would mean that these messages are only printed when a user explicitly requests it (and for bonus points: -constraintcheck=<list of symbols> to only check certain symbols).

> * If multiple __traits(constraintCheck) fail, do we print all?

I don't see any other way you could do it.


March 08, 2019
On Thursday, 7 March 2019 at 22:09:29 UTC, Johannes Pfau wrote:
> The DIP seems kind of intrusive to me. And Parsing a DNF

The DIP formalises a constraint in a _CNF_, not DNF.

> form has one major drawback: For quite some time now we actually advised people to refactor their constraints into external helper functions, in order to have nicer ddoc. E.g. isDigest!X instead of isOutputRange!X && ...

Yes, but this is for a single concept for a single parameter. Real constraints deal with multiple parameters and conjunctions and disjunction of concepts, hence the CNF formalisation. The rest of what you describe is great, but completely orthogonal.

I have made this comment before: https://github.com/dlang/DIPs/pull/131#issuecomment-416555780

> So now, we'd have to recommend the exact opposite.

This follows only for a single parameter.

March 08, 2019
On Wednesday, 6 March 2019 at 12:48:08 UTC, FeepingCreature wrote:
> Personally, I think the problem comes down to this: we have no way of propagating errors from  inside template constraints to the user.
>
> I think the way to go is instead of returning `false`, have __traits(compiles) (which is really the core source of these difficulties)

I find `is` expressions to be more to blame, but the underlying reason is the same: the error are gagged. This is good most of the time (because otherwise it would be like always using verrors=spec), but it misses the small fraction of the time when that is extremely useful.

> return an "error object" that evaluates to false, but also encodes additional information about the problem, recursively.
>
> Propagate this through short-circuiting - ErrorObject && bla = true, bla && ErrorObject = ErrorObject, etc.
>
> The compiler can then pick up on the fact that an error object was the reason why a template constraint failed to give a highly informative error message.
>
> For instance:
> *snip*

This works nicely for __traits compiles, I'm not so sure about is expressions (though I'd love to be shown otherwise).
March 08, 2019
On Friday, 8 March 2019 at 06:09:30 UTC, Nicholas Wilson wrote:
> This works nicely for __traits compiles, I'm not so sure about is expressions (though I'd love to be shown otherwise).

I'm not sure what the problem with `is` is? `is` is also designed to return false on compile failure and true on success, so replacing its return value with an ErrorObject doesn't seem like it *should* break things.
March 08, 2019
On Friday, 8 March 2019 at 06:34:00 UTC, FeepingCreature wrote:
> On Friday, 8 March 2019 at 06:09:30 UTC, Nicholas Wilson wrote:
>> This works nicely for __traits compiles, I'm not so sure about is expressions (though I'd love to be shown otherwise).
>
> I'm not sure what the problem with `is` is? `is` is also designed to return false on compile failure and true on success, so replacing its return value with an ErrorObject doesn't seem like it *should* break things.

But it also returns false if the expression is false

`is(T==int)` could return false because T is an error AST node, because T is undefined in the context or because T is not `int`. I'm missing how you would handle the latter case.