December 12
On Wed, 11 Dec 2024 at 08:25, Timon Gehr via Digitalmars-d < digitalmars-d@puremagic.com> wrote:

> On 12/9/24 07:47, Manu wrote:
> > On Mon, 9 Dec 2024 at 01:51, Timon Gehr via Digitalmars-d <digitalmars- d@puremagic.com <mailto:digitalmars-d@puremagic.com>> wrote:
> >
> >     On 12/8/24 06:54, Manu wrote:
> >      > Here's the stupidest idea ever: expand inout to take an
> argument...
> >      >
> >      > void parseThings(string s, void delegate(Thing) inout(nothrow)
> >      > inout(@nogc) sink) inout(nothrow) inout(@nogc)
> >      > {
> >      >    //...
> >      >    sink(Thing());
> >      >    //...
> >      > }
> >      > ...
> >
> >     Issue with this is you will still not know which `inout`s match up.
> >     This
> >     is already an issue with the existing `inout`, which just arbitrarily
> >     selects a convention. In particular, it does not really work with
> >     higher-order functions the way you'd need it to here.
> >
> >
> > Yes, this is obviously an issue, and we have it in D comprehensively;
> > it's the main issue with our 'safe' stuff too; where rust has explicit
> > lifetime attribution... it's essentially the same problem all round; it
> > needs tags that specify things which are associated.
> > In lieu of that though, I would say, if there are multiple things marked
> > inout(...) in the way I proposed, you would assume the most pessimistic
> > from the set of possibilities is supplied.
> >
> > void inheritAttribs(string s, void delegate() inout(nothrow)
> > inout(@nogc) fun_1, void delegate() inout(nothrow) inout(@nogc) fun_2)
> > inout(nothrow) inout(@nogc)
> > {
> >    //...
> >    fun_1();
> >    fun_2();
> >    //...
> > }
> >
> > In this case, `inheritAttribs` would only be nothrow in the event BOTH
> > fun_1 and fun_2 are nothrow, likewise for nogc...
> > ...
>
> Well, this is one limitation, but the `inout` on the delegate does not even match up with the `inout` on `inheritAttribs` those are already different.
>
> This does not compile;
>
> ```d
> inout(int)* foo(inout(int)* delegate()inout dg,inout(int)* x){
>      return x;
> }
>
> void main(){
>      const(int)* delegate()const dg;
>      const(int)* x;
>      foo(dg,x); // error
> }
> ```
>
> It seems at least this would need to work.
>
> `inout` is very confusing because different `inout` annotations can get conflated or not conflated. It's also the main reason why the implementation is unsound, it's not done consistently during type checking.
>

I agree that's a problem. It's classic D though where things are
non-uniform that way.
The resolution would probably be to always conflate all inouts; since we
can't tag them individually, we have to assume they're all uniformly
tagged; and they all take on the most restrictive state from the call.

>
> >      > Surely people have had better ideas? But you get the point, and
> >     this is
> >      > basically essential to make the 'sink' pattern work at all in D.
> >      >
> >      > No, no templates; it's not right to generate multiple copies of
> >     identical functions just because something it CALLS would transfer
> >     an attribute. The function itself is identical no matter the state
> >     of any attribute transference, and so this isn't a template problem;
> >     it's a pattern matching problem.
> >
> >     Well, it can be seen as a homogeneous vs heterogeneous compilation
> >     problem. The attributes can still act a bit like template parameters,
> >     but only one instance must be created that works for all of them.
> It's
> >     sometimes called parametric polymorphism.
> >
> >
> > Right. Do you have examples of this from other languages and how it's
> > expressed? Lifetime's in Rust are the obvious benchmark, but I don't see
> > any evidence that D has a taste for this sort of thing.
> > ...
>
> Well, Java, C#, Scala support homogeneous compilation in their generics.
>
> In Haskell everything is polymorphic by default with implicit universal quantification over lower-case type parameters.
>
> > I do think that inout could work for all attributes the same as inout does for const (with the same limitations).
>
> Well, as I showed above, those limitations are kind of fatal for your use case.
>

Sorry, I didn't follow what makes them fatal?

> You could see `inout` as
> > shorthand for `inout(const)` under my suggestion.
> > Obviously it must enforce the most restrictive implementation inside the
> > code; my function `inheritAttribs` above must be nothrow and nogc
> > internally, but externally it would allow a mapping from one of the
> > 'input' attributes to the 'output' attribute in a non-templated way.
>
> `inout` also interacts with `immutable`, not only `const`.
>

The way I saw it was that at compile time, `inout` is presumed to be
equivalent to const. You can't pass an inout(int)* to an immutable(int)*,
but you can pass to a const(int)*... but yeah, in terms of conceptual
precision, I see your point.


December 12
On Wed, 11 Dec 2024 at 08:41, Zach Tollen via Digitalmars-d < digitalmars-d@puremagic.com> wrote:

> On Sunday, 8 December 2024 at 05:54:40 UTC, Manu wrote:
> > We have a serious problem though; a function that receives a
> > sink function
> > must transfer the attributes from the sink function to itself,
> > and we have
> > no language to do that...
> > What are the leading strategies that have been discussed to
> > date? Is there
> > a general 'plan'?
>
> This was brought up in a [thread][thread1] in DIP Ideas. I contributed my idiosyncratic thoughts here:
>
> https://forum.dlang.org/post/rtvjsdyuqwmzwiggsolw@forum.dlang.org
>
> Suggestion #2 in that post addresses the issue you raise. In short, I proposed that the language default to inheriting attributes for the enclosing function at the call site, and then suggested a (callable function/delegate) parameter keyword `@noimply` for those rare cases when you *don't* want the function to inherit the characteristics of the delegate that you pass. I tried to explain it in the post.
>
> All my post did was suggest some hopefully elegant syntax for a number of attribute-related issues, of which this was one.
>
> [thread1]: https://forum.dlang.org/thread/pfawiqhppkearetcrkno@forum.dlang.org
>

This appears to suffer from the same category of problem as Schveighoffer's suggestion; it's that you can't reason about the function statically anymore. Again, the reason I approach it from the perspective of inout(...) is that you can always statically reason about it, and its guarantees are appropriately applied at compile time.


December 15

On Wednesday, 11 December 2024 at 22:36:27 UTC, Manu wrote:

>

On Wed, 11 Dec 2024 at 08:41, Zach Tollen via Digitalmars-d <

>

This was brought up in a [thread][thread1] in DIP Ideas. I contributed my idiosyncratic thoughts here:

https://forum.dlang.org/post/rtvjsdyuqwmzwiggsolw@forum.dlang.org

Suggestion #2 in that post addresses the issue you raise. In short, I proposed that the language default to inheriting attributes for the enclosing function at the call site, and then suggested a (callable function/delegate) parameter keyword @noimply for those rare cases when you don't want the function to inherit the characteristics of the delegate that you pass. I tried to explain it in the post.

This appears to suffer from the same category of problem as Schveighoffer's suggestion; it's that you can't reason about the function statically anymore.

I'm pretty sure you can. I think there's no avoiding the fact that the the call site is only place where the proper behavior for these calls can be determined. The best the function definition can do is to let the caller know (via e.g., @noimply) whether the function is able to negate any @system/throw/@gc/impure effects (the latter two should be understood with their obvious meaning).

For example, a function could negate these effects by not actually calling the delegate. Or by other means, which vary depending on the attribute. But since negation is the rare case, by default the language should assume, I believe, that any delegate/function that is passed in as an argument will be called. Which is to say, all the inout(...) spam in your suggestion would work — but it shouldn't need to: it should work implicitly. It makes sense: If you pass in a delegate which is @system/throw/@gc/impure, the call should be treated as a @system/throw/@gc/impure action, unless specifically negated.

>

Again, the reason I approach it from the perspective of inout(...) is that you can always statically reason about it, and its guarantees are appropriately applied at compile time.

My suggestion applies all the same guarantees at compile time. The negating effects of @noimply can be statically checked. For example:

void f(void delegate() sink) {
    sink();
}

void g() nothrow {
    f(() { throw new Exception(""); } ); // Error: function f() called with delegate which has been inferred `throw`, in `nothrow` function g()
}

Let's tell it not to imply that the sink throws/doesn't throw:

void h(void delegate() @noimply(throw) sink) {
    // The compiler tests the delegate by assuming sink() is @system/throw/@gc/impure
    sink(); // Error: delegate sink() is @noimply(throw), but may throw
}

The error above is the result of checking the delegate's attributes separately from the attributes for h(). This would happen for every use of a parameter delegate tagged with @noimply. (For parameters not tagged with @noimply, no checking is necessary — the call site propagates all the @system/throw/@gc/impure characteristics of the delegate that has been passed.)

In order to get it working, you have to make sure that the throw can't escape:

void j(void delegate() @noimply(throw) sink) {
    // passes, statically checks that the call to sink() takes place in a context that can't throw
    try { sink(); }
    catch(Exception) {}
}

void k() nothrow {
    // passes(!) because the parameter in j() has been statically confirmed to not propagate the throw
    j(() { throw new Exception(""); } );
}

I believe this system has all the benefits of the other suggestions, but with almost no syntactic noise.

December 15
On 15/12/2024 7:47 PM, Zach Tollen wrote:
> In order to get it working, you have to make sure that the throw can't escape:
> 
> |void j(void delegate() @noimply(throw) sink) { // passes, statically checks that the call to sink() takes place in a context that can't throw try { sink(); } catch(Exception) {} } void k() nothrow { // passes(!) because the parameter in j() has been statically confirmed to not propagate the throw j(() { throw new Exception(""); } ); } |
> 
> I believe this system has all the benefits of the other suggestions, but with almost no syntactic noise.

``nothrow`` is a very bad example to give here.

In fact, it needs an entirely separate mechanism from the others.

It isn't boolean, and has codegen changes associated with it.

Also in your example, the delegate could be marked as throwing and it would work today.

``nothrow`` is already scope aware.

December 15

On Sunday, 15 December 2024 at 07:04:54 UTC, Richard (Rikki) Andrew Cattermole wrote:

>

nothrow is a very bad example to give here.

In fact, it needs an entirely separate mechanism from the others.

It isn't boolean, and has codegen changes associated with it.

That's fair. I don't know if delegates work with exception handlers in a way that would permit this mechanism. Like, if a function is otherwise nothrow, but is called with a delegate parameter which throws, can the function be compiled to allow the throwing delegate to bypass the function it is called within and have the delegate throw straight to the calling function instead? Probably not, but I don't know enough about how modern exception handlers are programmed to know for sure.

>

Also in your example, the delegate could be marked as throwing and it would work today.

nothrow is already scope aware.

Under the current system, the following is an error:

// Error: function `g` may throw but is marked as `nothrow`
void g(void delegate() sink) nothrow {
    sink();
}

Under the new system (apart from the mechanism and codegen issues you raised), the above would be permitted... only if you called it with a throwing delegate would it be classified as throwing, and at the call site, but not in the function itself. This is much cleaner. I believe that it is primarily within this context that the new attribute @noimply would be occasionally useful, as a way to say that even if you call the nothrow function with a throwable delegate, the call can still be treated as nothrow, because @noimply(throw) statically guarantees that any incoming throw will be neutralized.

I'm pretty sure the purpose of @noimply would only become apparent if the new default were already adopted.

December 15
On 15/12/2024 10:08 PM, Zach Tollen wrote:
> On Sunday, 15 December 2024 at 07:04:54 UTC, Richard (Rikki) Andrew Cattermole wrote:
>> ``nothrow`` is a very bad example to give here.
>>
>> In fact, it needs an entirely separate mechanism from the others.
>>
>> It isn't boolean, and has codegen changes associated with it.
> 
> That's fair. I don't know if delegates work with exception handlers in a way that would permit this mechanism. Like, if a function is otherwise `nothrow`, but is called with a delegate parameter which throws, can the function be compiled to allow the throwing delegate to bypass the function it is called within and have the delegate throw straight to the calling function instead? Probably not, but I don't know enough about how modern exception handlers are programmed to know for sure.

Its not just how exception handlers work, but I also have to consider how a sum type exception handling mechanism would work. There is plenty of desire for it, and I've got a proposal in ideas for one.

Either way, it has codegen implications.

>> Also in your example, the delegate could be marked as throwing and it would work today.
>>
>> ``nothrow`` is already scope aware.
> 
> Under the current system, the following is an error:
> ```d
> // Error: function `g` may throw but is marked as `nothrow`
> void g(void delegate() sink) nothrow {
>      sink();
> }
> ```
> Under the new system (apart from the mechanism and codegen issues you raised), the above would be permitted... only if you called it with a throwing delegate would it be classified as throwing, and at the call site, but not in the function itself. This is much cleaner. I believe that it is primarily within this context that the new attribute `@noimply` would be occasionally useful, as a way to say that even if you call the `nothrow` function with a *throwable* delegate, the call can *still* be treated as `nothrow`, because `@noimply(throw)` statically guarantees that any incoming throw will be neutralized.
> 
> I'm pretty sure the purpose of `@noimply` would only become apparent if the new default were already adopted.

In your previous example you had the sink call wrapped in a try catch statement. That'll work.

```d
void f(void delegate() del) nothrow {
	try {
		del();
	} catch (Exception) {
	}
}
```

December 15

On Sunday, 15 December 2024 at 09:15:07 UTC, Richard (Rikki) Andrew Cattermole wrote:

>

In your previous example you had the sink call wrapped in a try catch statement. That'll work.

void f(void delegate() del) nothrow {
	try {
		del();
	} catch (Exception) {
	}
}

What would also work would be, only in the case of throw, to statically disallow passing a throwing delegate to a nothrow function. So:

// still allowed under the new system
void g(void delegate() sink) nothrow {
    sink();
}

void h() {
    g(() { throw new Exception(""); } ); // Error: `nothrow` function g() called with delegate which has been inferred `throw`
}
1 2 3
Next ›   Last »