September 08, 2020
On Monday, 7 September 2020 at 14:57:24 UTC, Steven Schveighoffer wrote:
> Thoughts?
>
> -Steve

I like your idea.

It's interesting that this would actually be implementable in the library if we had an ability to retrieve a template's compile-time parameters. Then you could have something like:

-----
void foo(T)(T v)
{
   auto x = v.someProperty;
}

template isCallable (alias sym, Args...)
{
    // create a syntactical "copy" of 'sym'
    void Proto (CompileTimeParams!sym)(RunTimeParams!sym)
    {
    }

    enum bool isCallable = is(typeof(Proto(Args)));
}

void bar(alias x)() if (isCallable!(x, 1))
{
    auto val = x(1);
}

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

However:

1. We don't have a way to extract the compile-time parameters from a template / templated function
2. I'm not sure if there is an easy way to retrieve the run-time parameters either. There is `ParameterStorageClassTuple` but it's awkward to use.

And regardless, a library solution will likely be very slow anyway. This belongs in the compiler.
September 08, 2020
On 9/8/20 1:03 AM, FeepingCreature wrote:
> On Monday, 7 September 2020 at 16:10:13 UTC, Steven Schveighoffer wrote:
>>
>> This is somewhat different than what I was proposing. However, it's very closely related.
>>
>> I absolutely need it to be matched with a function call, I don't want to have to do type acrobatics or check for UFCS, or whatever.
>>
>> However, I think both our needs are satisfied if the feature is something like checking to see that all symbols are resolved and that all instantiations are unambiguous. What happens after that should be considered a normal ungagged error.
>>
>> If I can pull out a non-template analogy, imagine you had code like:
>>
>> int foo()
>> {
>>    blahblah(); // unresolved symbol
>> }
>>
>> int bar()
>> {
>>    return foo();
>> }
>>
>> And instead of the error on the "blahblah" line, you got an error that foo doesn't exist, and so bar cannot be compiled. This is kind of how the current template state of affairs is. I want the same benefits for template code that non-template code has.
>>
> Yesss.
> 
> Okay, so as far as I can see there's two avenues that should give us both what we want. The first, and IMO best one (but W&A won't agree) is a huge breaking language change that errors in templates that have matched the constraints are always reported as errors, even in a gagged context.

No, the result of __traits(compiles) or is(typeof(...)) cannot be changed. I would never want that.

What we would need is a new feature that then we can migrate existing code to use.

> The second, more limited one that I think also gives you what you want (?) is a pragma *like* __traits(compiles), maybe __traits(instantiates), with the change that instead of gagging everything, it captures only one category of error - "the call couldn't find a matching function or template" - and only from the expression directly passed to it. All other errors are reported as usual.
> 
> Would that do it?

Yes, this is exactly what I'm looking for. I was going to call it __traits(canCall) or something, but instantiates makes perfect sense, since this only really is important for template instantiation.

In order to facilitate more complex conditions, I would say "the code in the expression passes semantic, with all template instantiations in the checked expression are matched" as the description.

For actual errors in the code being checked, it should return false just like __traits(compiles).

e.g.:

void foo()()
{
  import non.existent.mod;
}

__traits(compiles, foo()); // false
__traits(instantiates, foo()); // true, foo's body isn't checked for errors, just that it instantiates.
__traits(compiles, () {import non.existent.mod;} ()); // false
__traits(instantiates, () {import non.existent.mod;} ()); // false, the code inside the expression doesn't pass semantic

-Steve
September 08, 2020
On Tuesday, 8 September 2020 at 13:57:34 UTC, Steven Schveighoffer wrote:
>
> What we would need is a new feature that then we can migrate existing code to use.

On the other hand, improving the compiler to give more informative output about existing errors in existing code requires no migration and solves this entire category of problem, rather than just the subset involving IFTI or template argument deduction.

>> The second, more limited one that I think also gives you what you want (?) is a pragma *like* __traits(compiles), maybe __traits(instantiates), with the change that instead of gagging everything, it captures only one category of error - "the call couldn't find a matching function or template" - and only from the expression directly passed to it. All other errors are reported as usual.
>> 
>> Would that do it?
>
> Yes, this is exactly what I'm looking for. I was going to call it __traits(canCall) or something, but instantiates makes perfect sense, since this only really is important for template instantiation.

If we're going to take this approach, why stop here? Surely there are other categories of error that a programmer might want to query: __traits(parses), __traits(resolvesIdentifiers), __traits(typeChecks), __traits(safetyChecks), __traits(passesCtorFlowAnalysis), etc. Might as well go ahead and add them all.

The problem described in the OP of this thread is that the compiler fails to give the programmer useful information in certain situations. The solution is to improve the compiler so that it gives better information, not to add unprincipled hacks to the language so that the programmer can work around the compiler's deficiencies by hand.
September 08, 2020
On Mon, Sep 07, 2020 at 12:27:46PM -0400, Steven Schveighoffer via Digitalmars-d wrote: [...]
> I wasn't aware of -verrors=spec, but it sounds like it spitting out ALL speculative errors, not just when a specific call fails to build? While giving a searchable set of data, the situation is only slightly improved.

While it's great that we at least have -verrors=spec for those times when we're faced with a truly obtuse and impenetrable template error, IME it doesn't really improve the situation very much, because the real error is drowned in endless pages of other completely irrelevant errors before *and* after the relevant line(s).  There have been times when I'd rather resort to other methods of narrowing down the problem (like copy-n-pasting and stepwise truncating a long UFCS chain until the problem is isolated, or just plain ole rewrite the darned code in a way that *doesn't* involve so many templates) than to engage in yet another search for the needle of an error in a veritable haystack of irrelevant speculative template error fluff.


T

-- 
Philosophy: how to make a career out of daydreaming.
September 08, 2020
On 9/8/20 1:06 PM, Paul Backus wrote:
> On Tuesday, 8 September 2020 at 13:57:34 UTC, Steven Schveighoffer wrote:
>>
>> What we would need is a new feature that then we can migrate existing code to use.
> 
> On the other hand, improving the compiler to give more informative output about existing errors in existing code requires no migration and solves this entire category of problem, rather than just the subset involving IFTI or template argument deduction.

The error could cause subtle differences. The code could actually COMPILE, but not take the path intended. In which case the error is useless, even if it's more verbose.

But I would be able to live with better error messages for the current situation.

And by better error messages, I mean show me where my code failed to compile, not give me 5 pages of speculative compilation errors up a giant tree of calls.

> 
> If we're going to take this approach, why stop here? Surely there are other categories of error that a programmer might want to query: __traits(parses), __traits(resolvesIdentifiers), __traits(typeChecks), __traits(safetyChecks), __traits(passesCtorFlowAnalysis), etc. Might as well go ahead and add them all.

I'm looking to duplicate the error system for functions. That is, here is a function signature, don't worry about the innards, if you can call it with that signature, then you are done with the check. Then try to compile what you think should be valid, and if there's an error, just report that error.

I don't want any of those other features. I want the equivalent of "I only looked at the documentation, and called the function as specified."

> The problem described in the OP of this thread is that the compiler fails to give the programmer useful information in certain situations. The solution is to improve the compiler so that it gives better information

If you can solve that problem, then I'd be all for that.

> not to add unprincipled hacks to the language so that the programmer can work around the compiler's deficiencies by hand.

This would be the opposite of working around things by hand. I already do that today.

-Steve
September 08, 2020
On Tuesday, 8 September 2020 at 17:49:08 UTC, Steven Schveighoffer wrote:
> On 9/8/20 1:06 PM, Paul Backus wrote:
>> On Tuesday, 8 September 2020 at 13:57:34 UTC, Steven Schveighoffer wrote:
>>>
>>> What we would need is a new feature that then we can migrate existing code to use.
>> 
>> On the other hand, improving the compiler to give more informative output about existing errors in existing code requires no migration and solves this entire category of problem, rather than just the subset involving IFTI or template argument deduction.
>
> The error could cause subtle differences. The code could actually COMPILE, but not take the path intended. In which case the error is useless, even if it's more verbose.

I guess I've failed to communicate clearly. When I talk about "errors" here, unless I specifically say otherwise, I am referring exclusively to "top-level" non-speculative errors--the kind that DMD prints with red letters and that cause compilation to fail.

By definition, any code that contains such an error cannot compile, so the hypothetical you raise here is impossible.

> And by better error messages, I mean show me where my code failed to compile, not give me 5 pages of speculative compilation errors up a giant tree of calls.

Well, the compiler is already showing you where your code failed to compile: it failed in the template constraint, because `__traits(compiles, whatever)` evaluated to false. :)

What you and I really want is for the compiler to show us the *root cause* of that failure (which `-verrors=spec` currently succeeds at), and to do so without overwhelming us with irrelevant information (which `-verrors=spec` currently fails at).

>> The problem described in the OP of this thread is that the compiler fails to give the programmer useful information in certain situations. The solution is to improve the compiler so that it gives better information
>
> If you can solve that problem, then I'd be all for that.

If it's possible to selectively un-gag errors based on whether they happened during template argument deduction, it should also be possible to selectively un-gag errors based on whether they happened during template instantiation in general.

September 08, 2020
On 9/8/20 2:24 PM, Paul Backus wrote:
> On Tuesday, 8 September 2020 at 17:49:08 UTC, Steven Schveighoffer wrote:
>> The error could cause subtle differences. The code could actually COMPILE, but not take the path intended. In which case the error is useless, even if it's more verbose.
> 
> I guess I've failed to communicate clearly. When I talk about "errors" here, unless I specifically say otherwise, I am referring exclusively to "top-level" non-speculative errors--the kind that DMD prints with red letters and that cause compilation to fail.
> 
> By definition, any code that contains such an error cannot compile, so the hypothetical you raise here is impossible.

Like for instance:

struct S
{
   import std.range : isOutputRange;
   void toString(Output)(Output output) if (isOutputRange!(Output, dchar))
   {
      put(output, "hello, this is an S!"); // no import of std.range.put
   }
}

Now,

writeln(S.init) => S()

Wait, where's my specified message? I've literally spent hours on stuff like this, where I'm racking my brain trying to figure out why that function can't be called, only to realize that the function doesn't compile in the first place, when called with the specified constraints satisfied.

Whereas if writeln checked using the proposed __traits(instantiates) instead of __traits(compiles) on the call to S.toString(lockedTextOutput) (or whatever it does), then instead of the wrong path, I get a compilation error, telling me that my toString doesn't compile.

It's also possible that a compilation error happens in a path that isn't chosen because __traits(compiles) returns false in the path we expect, but the path taken instead doesn't have a __traits(compiles) guarding it. In which case, a weirder unexpected error message occurs far away from the true problem.

> Well, the compiler is already showing you where your code failed to compile: it failed in the template constraint, because `__traits(compiles, whatever)` evaluated to false. :)

In simple cases, this is enough. In complex cases, the error can be very far away from where the problem is.

> What you and I really want is for the compiler to show us the *root cause* of that failure (which `-verrors=spec` currently succeeds at), and to do so without overwhelming us with irrelevant information (which `-verrors=spec` currently fails at).

I essentially want to know why it made the decisions it made. So yes, in some cases, this might involve seeing ALL the speculative compilation output, and sifting through it. In the majority of cases, I want to know why it couldn't call my function foo()(int) with an int parameter, which is what it's telling me.

This would not solve all template compilation problems. Just most of them.

>> If you can solve that problem, then I'd be all for that.
> 
> If it's possible to selectively un-gag errors based on whether they happened during template argument deduction, it should also be possible to selectively un-gag errors based on whether they happened during template instantiation in general.

The point is not to ungag errors during argument deduction or gag them afterwards, but to stop trying to compile after deduction is done for that call. Essentially, don't actually instantiate the template, just see whether there is a matching template you would instantate.

-Steve
September 08, 2020
On Tuesday, 8 September 2020 at 18:59:00 UTC, Steven Schveighoffer wrote:
> Now,
>
> writeln(S.init) => S()
>
> Wait, where's my specified message? I've literally spent hours on stuff like this, where I'm racking my brain trying to figure out why that function can't be called, only to realize that the function doesn't compile in the first place, when called with the specified constraints satisfied.
>
> Whereas if writeln checked using the proposed __traits(instantiates) instead of __traits(compiles) on the call to S.toString(lockedTextOutput) (or whatever it does), then instead of the wrong path, I get a compilation error, telling me that my toString doesn't compile.

writeln could just as easily use __traits(hasMember) to check for the presence of toString, and print a warning message if it exists but doesn't compile. In other words, the problem here is that writeln assumes "toString doesn't compile" implies "toString isn't supposed to compile," which in most cases is not actually true. Adding a new language feature is neither necessary nor sufficient to correct that assumption.

I agree that it would probably be a good idea to change writeln (and other similar code) so that it does not make these kinds of assumptions.

> The point is not to ungag errors during argument deduction or gag them afterwards, but to stop trying to compile after deduction is done for that call. Essentially, don't actually instantiate the template, just see whether there is a matching template you would instantate.

From the programmer's perspective, is there any difference?

Let me make this concrete. Here's a contrived example program that illustrates the problem from your first post in this thread:

--- example.d
void use(alias fun)()
    if (__traits(compiles, fun()))
{}

void bad()()
{
    oops();
}

void irrelevant()()
{
    oops();
}

void test()
{
    enum _ = __traits(compiles, irrelevant());
    use!bad();
}
---

Here's what the compiler currently prints when you compile it:

---
example.d(18): Error: template instance `example.use!(bad)` does not match template declaration `use(alias fun)()`
  with `fun = bad()()`
  must satisfy the following constraint:
`       __traits(compiles, fun())`
---

Here's what the compiler currently prints when you compile it with `-verrors=spec`:
---
(spec:1) example.d(12): Error: undefined identifier `oops`
(spec:1) example.d(17): Error: template instance `example.irrelevant!()` error instantiating
(spec:1) example.d(17): Error: template instance `example.irrelevant!()` cannot resolve forward reference
(spec:1) example.d(17): Error: template `example.irrelevant` cannot deduce function from argument types `!()()`, candidates are:
(spec:1) example.d(10):        `irrelevant()()`
(spec:1) example.d(7): Error: undefined identifier `oops`
(spec:1) example.d(2): Error: template instance `example.bad!()` error instantiating
(spec:1) example.d(2): Error: template instance `example.bad!()` cannot resolve forward reference
(spec:1) example.d(2): Error: template `example.bad` cannot deduce function from argument types `!()()`, candidates are:
(spec:1) example.d(5):        `bad()()`
(spec:1) example.d(7): Error: undefined identifier `oops`
(spec:1) example.d(2): Error: template instance `example.bad!()` error instantiating
(spec:1) example.d(2): Error: template instance `example.bad!()` cannot resolve forward reference
(spec:1) example.d(2): Error: template `example.bad` cannot deduce function from argument types `!()()`, candidates are:
(spec:1) example.d(5):        `bad()()`
example.d(18): Error: template instance `example.use!(bad)` does not match template declaration `use(alias fun)()`
  with `fun = bad()()`
  must satisfy the following constraint:
`       __traits(compiles, fun())`
---

And here's what I would like the compiler to print with my proposed `-verrors=spec-but-only-stuff-i-care-about` flag:

---
(spec:1) example.d(7): Error: undefined identifier `oops`
(spec:1) example.d(2): Error: template instance `example.bad!()` error instantiating
(spec:1) example.d(2): Error: template instance `example.bad!()` cannot resolve forward reference
(spec:1) example.d(2): Error: template `example.bad` cannot deduce function from argument types `!()()`, candidates are:
(spec:1) example.d(5):        `bad()()`
(spec:1) example.d(7): Error: undefined identifier `oops`
(spec:1) example.d(2): Error: template instance `example.bad!()` error instantiating
(spec:1) example.d(2): Error: template instance `example.bad!()` cannot resolve forward reference
(spec:1) example.d(2): Error: template `example.bad` cannot deduce function from argument types `!()()`, candidates are:
(spec:1) example.d(5):        `bad()()`
example.d(18): Error: template instance `example.use!(bad)` does not match template declaration `use(alias fun)()`
  with `fun = bad()()`
  must satisfy the following constraint:
`       __traits(compiles, fun())`
---

Notice how the above includes messages about speculative errors in `bad`, but does NOT include messages about speculative errors in `irrelevant`.
September 08, 2020
On Tue, Sep 08, 2020 at 07:48:36PM +0000, Paul Backus via Digitalmars-d wrote: [...]
> writeln could just as easily use __traits(hasMember) to check for the presence of toString, and print a warning message if it exists but doesn't compile. In other words, the problem here is that writeln assumes "toString doesn't compile" implies "toString isn't supposed to compile," which in most cases is not actually true. Adding a new language feature is neither necessary nor sufficient to correct that assumption.
> 
> I agree that it would probably be a good idea to change writeln (and other similar code) so that it does not make these kinds of assumptions.
[...]

+1.  I think in the past, before the we had __traits(hasMember), __traits(compiles) was used as a kind of poor man's substitute, based on the shaky assumption that "it compiles" == "it has member X".  Now that we have better ways of expressing this, we should get rid of __traits(compiles) or other similar hacks based on similar shaky assumptions.


T

-- 
Life begins when you can spend your spare time programming instead of watching television. -- Cal Keegan
September 08, 2020
On 9/8/20 3:48 PM, Paul Backus wrote:

> 
> writeln could just as easily use __traits(hasMember) to check for the presence of toString, and print a warning message if it exists but doesn't compile. In other words, the problem here is that writeln assumes "toString doesn't compile" implies "toString isn't supposed to compile," which in most cases is not actually true. Adding a new language feature is neither necessary nor sufficient to correct that assumption.

hasMember is something I've been using a lot more lately. But it's not the same.

For instance, if the member was toString(int x), then it has the member toString, but would not match a call, and therefore should not be used. Contrived, but possible in other contexts.

Or the "member" could be a UFCS function, where __traits(hasMember) doesn't work.

> I agree that it would probably be a good idea to change writeln (and other similar code) so that it does not make these kinds of assumptions.

possibly with writeln, you can make the assumption that toString really should be picked even if it's not formed correctly. But that's not the case for other things.


> Let me make this concrete. Here's a contrived example program that illustrates the problem from your first post in this thread:
> 
> --- example.d
> void use(alias fun)()
>      if (__traits(compiles, fun()))
> {}
> 
> void bad()()
> {
>      oops();
> }
> 
> void irrelevant()()
> {
>      oops();
> }
> 
> void test()
> {
>      enum _ = __traits(compiles, irrelevant());
>      use!bad();
> }
> ---
> 

...

> And here's what I would like the compiler to print with my proposed `-verrors=spec-but-only-stuff-i-care-about` flag:
> 
> ---
> (spec:1) example.d(7): Error: undefined identifier `oops`
> (spec:1) example.d(2): Error: template instance `example.bad!()` error instantiating
> (spec:1) example.d(2): Error: template instance `example.bad!()` cannot resolve forward reference
> (spec:1) example.d(2): Error: template `example.bad` cannot deduce function from argument types `!()()`, candidates are:
> (spec:1) example.d(5):        `bad()()`
> (spec:1) example.d(7): Error: undefined identifier `oops`
> (spec:1) example.d(2): Error: template instance `example.bad!()` error instantiating
> (spec:1) example.d(2): Error: template instance `example.bad!()` cannot resolve forward reference
> (spec:1) example.d(2): Error: template `example.bad` cannot deduce function from argument types `!()()`, candidates are:
> (spec:1) example.d(5):        `bad()()`
> example.d(18): Error: template instance `example.use!(bad)` does not match template declaration `use(alias fun)()`
>    with `fun = bad()()`
>    must satisfy the following constraint:
> `       __traits(compiles, fun())`
> ---
> 
> Notice how the above includes messages about speculative errors in `bad`, but does NOT include messages about speculative errors in `irrelevant`.

Here's what it says when there is no constraint (and when I actually call fun inside use):

onlineapp.d(9): Error: undefined identifier oops
onlineapp.d(4): Error: template instance onlineapp.bad!() error instantiating
onlineapp.d(20):        instantiated from here: use!(bad)

This is what I want it to do. Why can't it just do that? Why do I need to see 10 more or even 5 more lines of irrelevant error messages?

-Steve