www.digitalmars.com         C & C++   DMDScript  

digitalmars.dip.ideas - Add __traits(canCall) and __traits(resolve)

reply Steven Schveighoffer <schveiguy gmail.com> writes:
Just having run into the problem of `__traits(compiles)` 
swallowing unexpected errors for the umpteenth time, I'm 
wondering if we can formally get this feature into the language.

This was a similar post by me a while back: 
https://forum.dlang.org/post/rj5hok$c6q$1 digitalmars.com

What I'd like to see are 2 traits calls.



This trait should return 0 if the expression is not a function 
call, or if the function call expression does not match any 
in-scope symbols.

It should return the number of matching symbols at the highest 
matching level. If this number is 1, then the call should be 
expected to resolve to a single correct function call.

If the number is greater than 1, then the code can expect that 
calling the symbol in this manner will result in an ambiguity 
error (as long as all functions are valid).

This does NOT compile the function, it just uses all existing 
compiler mechanisms to find the matches and select the correct 
option. Semantic errors in the function should not cause this to 
return 0 (the point is to avoid the issue with 
`__traits(compiles)`).



`__traits(compiles)` is often used to find whether a match to a 
call exists, but often times ends up with the confusing result of 
just ignoring the function altogether.

Everyone who has overloaded `toString` with an output range has 
had this experience:

```d
import std.range;
struct S
{
     int x;
     void toString(Out)(ref Out outputRange) if 
(isOutputRange!(Out, char))
     {
         // forgot to import std.format;
         outputRange.formattedWrite("x is %s", x);
     }
}

void main()
{
     import std.stdio;

     writeln(S(3)); // S(3), but expected x is 3
}
```

Debugging such things is difficult, because the "best effort" 
function `writeln` decides that `S.toString` doesn't exist, so it 
just doesn't bother calling it, and makes up its own formatting.

If instead, `writeln` used this trait, it could see that the 
`toString` call matches, and try to use it, producing an error 
the user can see and fix.

This does not fix errors in signature, or template constraints.

The experience of trying to implement hooks in D is significantly 
degraded due to this limitation, and I'm hoping this would 
improve the situation.



IFTI and other calls are tricky to predict. This traits should 
take a valid compilable call expression (no errors this time), 
and give you a symbol that the compiler would use to resolve the 
expression.

I'm expecting a result here for expressions that are call 
expressions only. For other expressions, I would maybe expect an 
error? I don't know. Are there other tricky situations that would 
benefit from this?

The result would be an alias to the resolving symbol determined 
by the IFTI or overload resolution algorithm.

I'm not 100% sure how to specify this. But something like:

```d
void foo(size_t opt = 42, K, V, T : K[V])(T val) {}

alias x = __traits(resolve, foo(int[int].init));
// equivalent to:
// alias x = foo!(42, int, int, int[int]);
```

Another example:

```d
void foo(long x) { }
void foo(string s) { }

auto getHandler(T)() {
    void function(T) x = &foo;
    return x;
}

void main()
{
     auto x = getHandler!int;
     x(1);
}
```

This produces an error, because there is no `foo` overload that 
exactly matches `int` as the parameter. What you really want 
inside `getHandler` is, give me the address of the foo overload 
that would be called if I passed in the argument of type `T`.

```d
// I'd like to write:
auto getHandler(T) {
     return &__traits(resolve, foo(T.init));
}
```



This feature unlocks introspection capabilities that are nearly 
impossible today. You can write the alias as above, but you can't 
tap into the machinery of IFTI or overload resolution (with 
conversions) without actually trying to call these items.

What this is saying is "give me the thing you would call if I 
wrote this expression".

These kinds of questions can be answered by the compiler, but we 
have no way of asking them in a satisfactory way.

-Steve
Jul 25
next sibling parent Dom DiSc <dominikus scherkl.de> writes:
On Saturday, 26 July 2025 at 02:27:08 UTC, Steven Schveighoffer 
wrote:
 Just having run into the problem of `__traits(compiles)` 
 swallowing unexpected errors for the umpteenth time, I'm 
 wondering if we can formally get this feature into the language.
[...]
 What I'd like to see are 2 traits calls.

 `__traits(canCall, expression)`
[...]
 `__traits(resolve, expression)`
[...]
 What this is saying is "give me the thing you would call if I 
 wrote this expression".

 These kinds of questions can be answered by the compiler, but 
 we have no way of asking them in a satisfactory way.
Yes, nice idea. I often wished something like this would be possible.
Jul 27
prev sibling next sibling parent reply Steven Schveighoffer <schveiguy gmail.com> writes:
On Saturday, 26 July 2025 at 02:27:08 UTC, Steven Schveighoffer 
wrote:


I was informed that opend has a feature similar to this proposal: __traits(resolveFunctionCall, overloadSet, argTypes...) While similar, it's not exactly what I want. Because it's not as natural as "what would you do if I wrote this expression". For example, this would not work well with UFCS functions. I'm unsure if it works with template functions. -Steve
Jul 28
parent Paul Backus <snarwin gmail.com> writes:
On Tuesday, 29 July 2025 at 02:11:22 UTC, Steven Schveighoffer 
wrote:
 On Saturday, 26 July 2025 at 02:27:08 UTC, Steven Schveighoffer 
 wrote:


I was informed that opend has a feature similar to this proposal: __traits(resolveFunctionCall, overloadSet, argTypes...) While similar, it's not exactly what I want. Because it's not as natural as "what would you do if I wrote this expression".
Also, taking a list of types instead of an actual argument list means that you cannot distinguish between by-`ref` and by-value overloads of the same function.
Jul 29
prev sibling next sibling parent reply "Richard (Rikki) Andrew Cattermole" <richard cattermole.co.nz> writes:
Something I've wanted for along time now, to speed up reflection is the 
offering to filter symbols.

```d
__traits(filterFunctionByTypes, source, Return, Parameters...)
```

Where ``source`` could be a type, alias (overload set), named import ext.

Where ``Return`` could be ``auto`` to mean "don't care".

But also:

```d
__traits(filterFunctionByExpression, source, args...)
```

The result of both is an alias sequence of symbols.
Jul 28
parent reply Steven Schveighoffer <schveiguy gmail.com> writes:
On Tuesday, 29 July 2025 at 04:28:44 UTC, Richard (Rikki) Andrew 
Cattermole wrote:
 Something I've wanted for along time now, to speed up 
 reflection is the offering to filter symbols.

 ```d
 __traits(filterFunctionByTypes, source, Return, Parameters...)
 ```

 Where ``source`` could be a type, alias (overload set), named 
 import ext.

 Where ``Return`` could be ``auto`` to mean "don't care".

 But also:

 ```d
 __traits(filterFunctionByExpression, source, args...)
 ```

 The result of both is an alias sequence of symbols.
This is a tough one to implement, because of the auto thing. Is there a reason you need to avoid passing actual types? This also doesn't seem to be designed to handle templates? -Steve
Jul 29
parent "Richard (Rikki) Andrew Cattermole" <richard cattermole.co.nz> writes:
On 30/07/2025 2:48 AM, Steven Schveighoffer wrote:
 On Tuesday, 29 July 2025 at 04:28:44 UTC, Richard (Rikki) Andrew 
 Cattermole wrote:
 Something I've wanted for along time now, to speed up reflection is 
 the offering to filter symbols.

 ```d
 __traits(filterFunctionByTypes, source, Return, Parameters...)
 ```

 Where ``source`` could be a type, alias (overload set), named import ext.

 Where ``Return`` could be ``auto`` to mean "don't care".

 But also:

 ```d
 __traits(filterFunctionByExpression, source, args...)
 ```

 The result of both is an alias sequence of symbols.
This is a tough one to implement, because of the auto thing. Is there a reason you need to avoid passing actual types?
It would be types. Its just that auto would be consumed to mean ignore return since its a required argument.
 This also doesn't seem to be designed to handle templates?
 
 -Steve
Some should work, but others may need more control to do the filtering as you want it to work. The main thing is that its a transfer function, take in alias sequence, give an alias sequence. It'll apply in a lot more cases if you use this form.
Jul 29
prev sibling next sibling parent Steven Schveighoffer <schveiguy gmail.com> writes:
On Saturday, 26 July 2025 at 02:27:08 UTC, Steven Schveighoffer 
wrote:

Although we came up with it independently, I have to give credit to Paul Backus to coming up with this first! Now I know I got the right idea ;) And even the name matches! https://forum.dlang.org/post/atjkxxmugmfbsntvwgpi forum.dlang.org -Steve
Jul 29
prev sibling next sibling parent Atila Neves <atila.neves gmail.com> writes:
On Saturday, 26 July 2025 at 02:27:08 UTC, Steven Schveighoffer 
wrote:
 Just having run into the problem of `__traits(compiles)` 
 swallowing unexpected errors for the umpteenth time, I'm 
 wondering if we can formally get this feature into the language.

 [...]
Both make sense to me. I've also lost count of how many times I've had to debug why `__traits(compiles)` was failing.
Jul 31
prev sibling parent reply Nick Treleaven <nick geany.org> writes:
On Saturday, 26 July 2025 at 02:27:08 UTC, Steven Schveighoffer 
wrote:


 This trait should return 0 if the expression is not a function 
 call, or if the function call expression does not match any 
 in-scope symbols.

 It should return the number of matching symbols at the highest 
 matching level. If this number is 1, then the call should be 
 expected to resolve to a single correct function call.

 If the number is greater than 1, then the code can expect that 
 calling the symbol in this manner will result in an ambiguity 
 error (as long as all functions are valid).

 This does NOT compile the function, it just uses all existing 
 compiler mechanisms to find the matches and select the correct 
 option. Semantic errors in the function should not cause this 
 to return 0 (the point is to avoid the issue with 
 `__traits(compiles)`).
Regarding implementation, I infer that the body of the function should not be analysed for this trait. That means attributes won't be inferred, so the call actually may not compile even if the body has no error. I.e. the body may be safe but because the function is not marked safe, `canCall` has to give false when used in a safe context. Otherwise, how would it work?
Aug 01
next sibling parent Nick Treleaven <nick geany.org> writes:
On Friday, 1 August 2025 at 10:21:53 UTC, Nick Treleaven wrote:
 Regarding implementation, I infer that the body of the function 
 should not be analysed for this trait. That means attributes 
 won't be inferred, so the call actually may not compile even if 
 the body has no error. I.e. the body may be  safe but because 
 the function is not marked  safe, `canCall` has to give false 
 when used in a  safe context. Otherwise, how would it work?
Alternatively, if function attributes should be ignored, perhaps the trait shouldn't be called 'canCall', but 'canResolve'?
Aug 01
prev sibling parent Steven Schveighoffer <schveiguy gmail.com> writes:
On Friday, 1 August 2025 at 10:21:53 UTC, Nick Treleaven wrote:
 Regarding implementation, I infer that the body of the function 
 should not be analysed for this trait. That means attributes 
 won't be inferred, so the call actually may not compile even if 
 the body has no error. I.e. the body may be  safe but because 
 the function is not marked  safe, `canCall` has to give false 
 when used in a  safe context. Otherwise, how would it work?
Correct. The point is to hook the lookup rules only. But then you should get the error "you tried to call this unsafe function from this safe function, here's the error:..." This is more desirable than compilation of wrong things. Maybe `canCall` is not the greatest name. Maybe `canResolve`? I don't know. The point is to avoid the situation where code has a bug in it, but is swallowed by `__traits(compiles)`. This is incredibly frustrating, because you are looking at code that *should compile* so you think, but it is completely ignoring it. The `toString` example is very common. Due to the nature of templates especially, things don't show up unless you use it. But basing introspection on "does this thing and everything it uses compile" is just too blunt of an instrument. -Steve
Aug 01