www.digitalmars.com         C & C++   DMDScript  

digitalmars.dip.development - Uncallable delegates

reply Dukc <ajieskola gmail.com> writes:
I have (finally) finished my DIP draft for the [Delegates and 
qualifier 
transitivity](https://forum.dlang.org/thread/gbrwklyugksqbpksg
gp forum.dlang.org) idea discussion.

It can be found 
[here](https://gist.github.com/dukc/0ede11a0fcb77b2ff92654ceaa7805e2), and is
awaiting your feedback.
Apr 01
parent reply Walter Bright <newshound2 digitalmars.com> writes:
On 4/1/2026 7:29 AM, Dukc wrote:
 It can be found 
 [here](https://gist.github.com/dukc/0ede11a0fcb77b2ff92654ceaa7805e2), and is 
 awaiting your feedback.
Thank you for your hard work on this. It's an important thing to get right. The difficulty I have with it is I do not understand it. It has to be broken down into simpler constructs, then built back up into the delegate. How delegates behave is inevitable, it is something that we either get right or get severely wrong. ** Implicit Conversions Let's start with implicit conversions: ``` int* p; const(int)* q; p = q; // invalid q = p; // valid (contravariance) ``` What is important here is all of the type qualifiers add a *restriction*. None of the the type qualifiers *loosen* it. So the implicit conversions only go one way, like a diode. ** Class Inheritance The next concept is covariance: ``` class A { void foo(int*); void bar(const(int)*); } class B : A { void foo(const(int)*); // ok - added restriction, covariant void bar(int*); // error, removed restriction } ``` Since B.foo overrides A.foo, it must adhere to the (int*) constraint, and since nothing says A.foo must modify through the parameter, overriding it with B.foo that promises to not change it, still fulfills the (int*).B.bar() is an error because it does not fulfill the promise of A.bar() that the argument won't be modified. This is called covariance. Next, contravariance: ``` class A { int* foo(); const(int)* bar(); } class B { const(int)* foo(); // error, as the return value gets interpreted as mutable int* bar(); // ok, as the return result can be implicitly cast to const } ``` This property is called contra-variance. ** Function Pointers ``` int* function() foop; const(int)* function() barp; barp = foop; // ok, contravariant, foop()'s return is cast to const(int)* foop = barp; // error, barp's return is cast to mutable ``` ** Delegates Delegates are a function pointer with an additional (hidden) parameter. ``` int* delegate() foodg; ``` For illustration purposes, it can be rewritten as a function pointer with the additional (hidden) parameter explicit, let's call it `this`: ``` int* function(const(int)* this) foodg; ``` Apply the rules for return types for function pointers, and the covariant rules for the `this` parameter, and the behavior of delegates is completely derived from the earlier rules. We are not designing anything new. *** Type Qualifiers for Delegates ``` const T* delegate() dg; // the const applies to the function pointer const(T)* delegate() dg; // the const(T)* is the type of the return value void delegate() const dg; // the const is applied to the `this` parameter ``` It is not necessary for the DIP to address any storage classes or attributes other than simply use `pure` for storage class and `const` for attribute. The behavior of the other storage classes (like nothrow, nogc) is the same. The same for other attributes (immutable, safe, nothrow, etc.) Feel free to use any or all of this in your DIP.
May 11
parent reply Dukc <ajieskola gmail.com> writes:
On Tuesday, 12 May 2026 at 04:29:12 UTC, Walter Bright wrote:
 Thank you for your hard work on this. It's an important thing 
 to get right.
For context to anyone else following: the feedback phase has actually ended. The DIP is now in formal assessment and Walter is asking for clarifications.
 The difficulty I have with it is I do not understand it. It has 
 to be broken down into simpler constructs, then built back up 
 into the delegate. How delegates behave is inevitable, it is 
 something that we either get right or get severely wrong.
Let's start with your current intuition on delegates. You wrote that your mental picture of a delegate is, roughly speaking, this: ```D struct _delegate(FP) if (isFunctionPointer!FP) { // AddContextPointer is an imaginary template that returns a function // pointer passed to it with one parameter added for the context pointer. AddContextPointer!FP funPointer; void* context; auto opCall(Parameters!FP args) => funPointer(args, context); } ``` This is all right and good as long as we're considering only ` system` code. However, for ` safe` code there are additional considerations. First off, both `funPointer` and `context` are ` system` fields. This means you can't access them directly in safe code, nor you can't change them indepentently of each other, just like you can't access or change the pointer of an array indepentely of it's length. Second, all safe delegates hold an important invariant: it is safe to pass the context to the pointed function when calling it. When the delegate is typed as `const` (for instance), it follows the context is also `const`. Therefore the function behind `funPointer` of any `const` delegate must not mutate the context through the context pointer, otherwise the invariant is broken and we have an unsafe delegate. Now, there are some practically unavoidable corner cases where such unsafe delegates can be created. Consider an abstract class: ```D class A { abstract safe void foo() const; } ``` If you convert an object of `A` to `const(A)`, this could be problematic, because the concrete class of the object could be ```D class B : A { int field; safe void delegate() del; safe void increment(){field++;} safe void foo() const {del();} } ``` and `del` might point `increment` and context to the object itself. The result would be that calling `foo` mutates the object in violation of the `const` qualification. Because of this, the DIP specifies that if the delegate as a whole has qualifiers that are incompatible with the context qualification of the function pointer, the delegate cannot be guaranteed to hold the invariant and thus cannot be called. In this example, `B.foo` would fail to compile, because it's calling a delegate typed `const( safe void delegate())`. You would either have to type `del` as ` safe void delegate() const` or remove `const` qualification from `foo`. Note that the opposite situation is fine. You are allowed to call a mutable delegate pointing to a function that requires an immutable context. This is safe because of the first invariant. Only the function behind the pointer has access to the context via the pointer in the delegate, so there's nothing that could mutate the context through it (other than buggy ` trusted` code).
May 12
next sibling parent Dukc <ajieskola gmail.com> writes:
On Tuesday, 12 May 2026 at 11:13:15 UTC, Dukc wrote:
 This is safe because of the first invariant.
Meant: because the fields of the delegate are not accessible from outside in safe code.
May 12
prev sibling next sibling parent reply Walter Bright <newshound2 digitalmars.com> writes:
I think of the issue completely differently. Allow me to try again, starting 
with something simpler, like function pointers:

```
pure int f();
int g();

void test()
{
     auto dgf = &f;
     auto dgg = &g;

     dgg = dgf; // works
     dgf = dgg; // error, cannot convert
}
```
This shows that we cannot add function attributes, in this case adding `pure`. 
The same applies to every other function attribute. This is not a design 
decision, it's an inevitable deriviation from the other rules of the language.

Another case:
```
const(int)* f();
int* g();

void test()
{
     auto dgf = &f;
     auto dgg = &g;

     dgg = dgf; // error, cannot convert
     dgf = dgg; // works
}
```
This is an example of contra-variance.
```
void f(const(int)*);
void g(int*);

void test()
{
     auto dgf = &f;
     auto dgg = &g;

     dgg = dgf; // works
     dgf = dgg; // error, cannot convert
}
```
and that's an example of covariance.

Let's extend the notion to inheritance in a class:

```
class A
{
     void f();
     pure void g();
}

class B : A
{
     override pure void f();
     override void g();
}
```
Unfortunately, this compiles without error. Impure function B.g cannot override 
pure A.g. Maybe this is a cause of one of the problems you've discovered.
https://github.com/dlang/dmd/issues/23125

Moving on to covariance:
```
class A
{
     void f(int*);
     void g(const(int)*);
}

class B : A
{
     override void f(const(int)*); // compiles correctly
     override void g(int*); // errors correctly
}
```
and contravariance:
```
class A
{
     int* f();
     const(int)* g();
}

class B : A
{
     override const(int)* f(); // errors correctly
     override int* g(); // compiles correctly
}
```
What this has to do with delegates is straightforward:
```
class A
{
     const(int)* f();
}

class B : A
{
     override int* f();
}

void foo()
{
     A a;
     B b;
     auto dga = &a.f;
     auto dgb = &b.f;
     dga = dgb;
}
```
In other words, the behavior of delegate assignments behaves exactly like how
an 
overriding function behaves in a class hierarchy. If the behaviors diverge,
then 
there is a bug in the language implementation.

Delegates are not some unique construct with their own semantics. They are 
constrained by co- and contra- variance eggzactly like function pointers and 
class inheritance. Intuition has nothing to do with it.
May 12
parent reply Dukc <ajieskola gmail.com> writes:
On Tuesday, 12 May 2026 at 23:18:30 UTC, Walter Bright wrote:
 The same applies to every other function attribute. This is not 
 a design decision, it's an inevitable deriviation from the 
 other rules of the language.
So is that some delegates must be uncallable. I'll demonstrate, starting with function pointers and avoiding bringing ` safe`/`pure` or templates to table this time. Let's rewrite a very simple delegate, `void delegate(int)`, to a function pointer pair. ```D struct ExampleDel { void function(int, void*) fPtr; void* context; this(void function(int, void*) fPtr, void* context) { this.fPtr = fPtr; this.context = context; } void opCall(int arg) => fPtr(arg, context); } struct S { string field; static void memberFun(int arg, void* _this) { import std.stdio; writeln((*cast(typeof(this)*) _this).field, arg); } } void main() { auto s = S("val = "); auto del = ExampleDel(&s.memberFun, &s); del(24); // val = 24 } ``` Now, what happens if we try to make `del` `const`, making it the equivalent of `const(void delegate(int))`? ```D void main() { auto s = S("val = "); const del = ExampleDel(&s.memberFun, &s); del(24); } ``` It doesn't compile: ``` Error: mutable method `app.ExampleDel.opCall` is not callable using a `const` object del(24); ``` Neither can we qualify `opCall` as `const`: ``` Error: function pointer `this.fPtr` is not callable using argument types `(int, const(void*))` void opCall(int arg) const => fPtr(arg, context); ^ cannot pass argument `this.context` of type `const(void*)` to parameter `void* ``` These errors are not bugs. If you use the call operator of a `const` struct, of course the struct has to actually support that. And `context` getting typed as `const(void*)` once `opCall` has a `const` context is `const` transitivity working as intended. We can conclude that calling `const(void delegate(int))` is fundamentally unsound. It can't be made to work without type system breaking casts. Note that this does not apply to `const(void delegate(int) const)`. The rewrite works: ```D struct ExampleDel { void function(int, const(void*)) fPtr; void* context; this(void function(int, const(void*)) fPtr, void* context) { this.fPtr = fPtr; this.context = context; } void opCall(int arg) const => fPtr(arg, context); } struct S { string field; static void memberFun(int arg, const(void*) _this) { import std.stdio; writeln((*cast(typeof(this)*) _this).field, arg); } } void main() { auto s = S("val = "); const del = ExampleDel(&s.memberFun, &s); del(24); // val = 24 } ```
May 13
parent reply Walter Bright <newshound2 digitalmars.com> writes:
On 5/13/2026 12:32 AM, Dukc wrote:
 We can conclude that calling `const(void delegate(int))` is fundamentally 
 unsound. It can't be made to work without type system breaking casts.
"Unsound" means it leads to corruption of the type system, such as being able to modify an immutable value. Unsound would mean something like 2+2=5. If I understand what you're saying, the issue here is applying the `const` type specifier not being applied to the argument to the delegate's `context` pointer. Consider: ``` int abc(int*); auto fp = const(&abc); ``` This means we cannot rebind fp to another function. It does not mean fp's function parameter is now `const int*`. In other words, transitivity does not apply to the parameter list.
 Note that this does not apply to const(void delegate(int) const).
That's exactly as intended. The second const applies to the hidden context parameter. There has been endless confusion in D about attributes being attached to the implicit `this` pointer. A great deal of the dissatisfaction about dip1000 is attributable to this. I've been considering a proposal to enable being able to explicitly list the hidden parameter in the argument list, as it then makes it straightforward to attach attributes to it.
May 13
next sibling parent reply Dukc <ajieskola gmail.com> writes:
On Wednesday, 13 May 2026 at 20:06:29 UTC, Walter Bright wrote:
 If I understand what you're saying, the issue here is applying 
 the `const` type specifier not being applied to the argument to 
 the delegate's `context` pointer.
Yes, this is what causes the error - and rightly so!
 Consider:
 ```
 int abc(int*);
 auto fp = const(&abc);
 ```
 This means we cannot rebind fp to another function.

 It does not mean fp's function parameter is now `const int*`.

 In other words, transitivity does not apply to the parameter 
 list.
Exactly. I think we're now talking about the same thing.
 Note that this does not apply to const(void delegate(int)
const). That's exactly as intended. The second const applies to the hidden context parameter.
Yes it is. Returning to the DIP, the issue with the present rules are that calling a `const` delegate (or equally `immutable`, `shared` or `inout` delegate) without the `const` for the context parameter is allowed. It shouldn't be, just like the equivalent function pointer rewrite we just discussed isn't. My DIP is proposing disallowing it, along with changes to the qualifier casting rules of delegates.
 I've been considering a proposal to enable being able to 
 explicitly list the hidden parameter in the argument list, as 
 it then makes it straightforward to attach attributes to it.
This is orthogonal to this discussion. It might help, but you can specify parameters for the context parameter even with the present language and we both understand the existing syntax.
May 13
parent reply Walter Bright <newshound2 digitalmars.com> writes:
On 5/13/2026 1:30 PM, Dukc wrote:
 Returning to the DIP, the issue with the present rules are that calling a 
 `const` delegate (or equally `immutable`, `shared` or `inout` delegate)
without 
 the `const` for the context parameter is allowed. It shouldn't be, just like
the 
 equivalent function pointer rewrite we just discussed isn't. My DIP is
proposing 
 disallowing it, along with changes to the qualifier casting rules of delegates.
I'm glad we are understanding each other better. I'm not convinced the proposed change improves things.
May 14
parent reply Timon Gehr <timon.gehr gmx.ch> writes:
On 5/14/26 21:34, Walter Bright wrote:
 On 5/13/2026 1:30 PM, Dukc wrote:
 Returning to the DIP, the issue with the present rules are that 
 calling a `const` delegate (or equally `immutable`, `shared` or 
 `inout` delegate) without the `const` for the context parameter is 
 allowed. It shouldn't be, just like the equivalent function pointer 
 rewrite we just discussed isn't. My DIP is proposing disallowing it, 
 along with changes to the qualifier casting rules of delegates.
I'm glad we are understanding each other better. I'm not convinced the proposed change improves things.
FWIW, I am convinced that Dukc has the right idea about delegate typing, and the proposed change certainly improves things. Though I guess the unsound `pure` factory function interaction will remain intact with the DIP as written. (Which is partially my bad, I did not consider the potential convertibility from mutable to `immutable` when deriving the soundness condition in the idea thread.)
May 14
parent reply Walter Bright <newshound2 digitalmars.com> writes:
On 5/14/2026 2:55 PM, Timon Gehr wrote:
 FWIW, I am convinced that Dukc has the right idea about delegate typing, and
the 
 proposed change certainly improves things. Though I guess the unsound `pure` 
 factory function interaction will remain intact with the DIP as written.
(Which 
 is partially my bad, I did not consider the potential convertibility from 
 mutable to `immutable` when deriving the soundness condition in the idea
thread.)
I am skeptical that this proposed behavior does not match how overrides in classes work.
May 14
parent reply Timon Gehr <timon.gehr gmx.ch> writes:
On 5/15/26 04:23, Walter Bright wrote:
 On 5/14/2026 2:55 PM, Timon Gehr wrote:
 FWIW, I am convinced that Dukc has the right idea about delegate 
 typing, and the proposed change certainly improves things. Though I 
 guess the unsound `pure` factory function interaction will remain 
 intact with the DIP as written. (Which is partially my bad, I did not 
 consider the potential convertibility from mutable to `immutable` when 
 deriving the soundness condition in the idea thread.)
I am skeptical that this proposed behavior does not match how overrides in classes work.
Why would the callability check match the overriding behavior? The callability check should match the callability check. ```d class C{ void foo(){} } void main(){ const c = new C; c.foo(); // error } ``` Specifically which proposed behavior are you not able to explain in terms of classes?
May 14
parent reply Walter Bright <newshound2 digitalmars.com> writes:
On 5/14/2026 7:36 PM, Timon Gehr wrote:
 Specifically which proposed behavior are you not able to explain in terms of 
 classes?
The overriding of class functions must behave the same as implicit conversions on delegates.
May 15
next sibling parent reply Dukc <ajieskola gmail.com> writes:
On Saturday, 16 May 2026 at 02:05:17 UTC, Walter Bright wrote:
 On 5/14/2026 7:36 PM, Timon Gehr wrote:
 Specifically which proposed behavior are you not able to 
 explain in terms of classes?
The overriding of class functions must behave the same as implicit conversions on delegates.
The DIP is proposing two things: changes to delegate qualifier conversion rules, and disallowing calling of delegates of unsound types (like `const(void delegate(int))` we discussed. Both changes are necessary for type safety, but they still are different changes that could be done independently of each other. I will analyse your class inheritance analogy later when I have more time, likely this evening. But, if I understand right, your reservations are specifically about the conversion rules. Are you already sold on the need to make some delegates uncallable, or do you have also have some other concerns regarding them?
May 15
parent reply Walter Bright <newshound2 digitalmars.com> writes:
On 5/15/2026 9:55 PM, Dukc wrote:
 I will analyse your class inheritance analogy later when I have more time, 
 likely this evening. But, if I understand right, your reservations are 
 specifically about the conversion rules. Are you already sold on the need to 
 make some delegates uncallable, or do you have also have some other concerns 
 regarding them?
My concern is this problem is a problem with the implicit conversion of a delegate to `pure`. I suggest that problem get fixed before going any further with this. The reason is the bug examples seem to rely on such an implicit conversion. If the same problem can be demonstrated without using pure attributes, then it is a problem separate from the `pure` inheritance issue. Forgive me for asserting that many, many of the issues that have been posted to bugzilla turned out to not be the problem the submitter thought it was, but something unexpected which was revealed when the problem submission was pared down to a minimal example.
May 15
next sibling parent reply Timon Gehr <timon.gehr gmx.ch> writes:
On 5/16/26 08:47, Walter Bright wrote:
 On 5/15/2026 9:55 PM, Dukc wrote:
 I will analyse your class inheritance analogy later when I have more 
 time, likely this evening. But, if I understand right, your 
 reservations are specifically about the conversion rules. Are you 
 already sold on the need to make some delegates uncallable, or do you 
 have also have some other concerns regarding them?
My concern is this problem is a problem with the implicit conversion of a delegate to `pure`.
There is no implicit conversion of a delegate to `pure` in the example.
 I suggest that problem get fixed before going any 
 further with this. The reason is the bug examples seem to rely on such 
 an implicit conversion.
 ...
No, the example I gave you just relies on the result of a `pure` function call being implicitly convertible to `immutable`. There is no implicit conversion of a delegate type to `pure`, you clearly did not actually look into it at all. Why are you not looking at the example? It is short.
 If the same problem can be demonstrated without using pure attributes, 
 then it is a problem separate from the `pure` inheritance issue.
 ...
The premise is false, and the implication only goes in one direction.
 Forgive me for asserting that many, many of the issues that have been 
 posted to bugzilla turned out to not be the problem the submitter 
 thought it was, but something unexpected which was revealed when the 
 problem submission was pared down to a minimal example.
I gave you a minimal example for the `immutable` case. This case requires a `pure` factory function, because that is the only ` safe` way to get an `immutable` reference from something that used to be mutable. The same type system unsoundness exists for `const`. However, as far as I understand, the optimizer does not optimize based on `const` UB (and even then it would probably additionally rely on `pure` for alias analysis and distract you), so I can't give you an executable `2+2 == 5` case. The best I can do right now is show you that the delegate type checking is different from classes: ```d safe: struct T{ int* delegate() dg; int* q; } T foo(){ auto x = new int(2); auto dg = ()=>x; return T(dg,x); } void main(){ const ps = foo(); static assert(is(typeof(ps.dg)==const)); auto p = ps.dg(); static assert(is(typeof(p)==int*)); // `const` removed } ``` Here is what it will do with a class instead of a delegate: ```d safe: class C{ int* x; this(int* x){ this.x=x; } int* call(){ return x; } } struct T{ C dg; int* q; } T foo(){ auto x = new int(2); auto dg = new C(x); return T(dg,x); } void main(){ const ps = foo(); static assert(is(typeof(ps.dg)==const)); auto p = ps.dg.call(); // error //static assert(is(typeof(p)==int*)); // `const` removal not allowed } ```
May 16
parent Walter Bright <newshound2 digitalmars.com> writes:
Thank you. I will come back to this after I get the fixups working right on the 
AArch64 code gen.
May 18
prev sibling next sibling parent Timon Gehr <timon.gehr gmx.ch> writes:
On 5/16/26 08:47, Walter Bright wrote:
 My concern is this problem is a problem with the implicit conversion of 
 a delegate to `pure`.
There is no such thing as an implicit conversion of a delegate to `pure`.
May 16
prev sibling parent Dukc <ajieskola gmail.com> writes:
On Saturday, 16 May 2026 at 06:47:21 UTC, Walter Bright wrote:
 On 5/15/2026 9:55 PM, Dukc wrote:
 I will analyse your class inheritance analogy later when I 
 have more time, likely this evening. But, if I understand 
 right, your reservations are specifically about the conversion 
 rules. Are you already sold on the need to make some delegates 
 uncallable, or do you have also have some other concerns 
 regarding them?
My concern is this problem is a problem with the implicit conversion of a delegate to `pure`. I suggest that problem get fixed before going any further with this. The reason is the bug examples seem to rely on such an implicit conversion.
Like Timon wrote, there aren't actually any problems (AFAIK) with `pure` conversions. The DIP also doesn't deal with `pure` - only with the type qualifiers. The examples in the DIP do use `pure`, but just to demonstrate how reasonable compiler optimisations could exploit the broken type system. You can remove `pure` from the examples and the type system fault itself - namely, immutable data getting mutated - is still there.
May 16
prev sibling next sibling parent Timon Gehr <timon.gehr gmx.ch> writes:
On 5/16/26 04:05, Walter Bright wrote:
 On 5/14/2026 7:36 PM, Timon Gehr wrote:
 Specifically which proposed behavior are you not able to explain in 
 terms of classes?
The overriding of class functions must behave the same as implicit conversions on delegates.
This is not correct. You can make it work that way and it would at least be sound, but it is also really weird. Consider: ```d void main(){ immutable x = new int(2); auto foo()immutable => x; auto bar() => x; auto dg1 = &foo; auto dg2 = &bar; //dg2 = dg1; // error auto baz() => dg1(); // ok dg2 = &baz; // ok } ``` There is no reason at all to disallow the first assignment. Delegates are not the same as class methods. The reason is that class methods accept an external `this` pointer and delegates have an internal `this` pointer. This also affects conversions.
May 15
prev sibling parent Dukc <ajieskola gmail.com> writes:
On Saturday, 16 May 2026 at 02:05:17 UTC, Walter Bright wrote:
 The overriding of class functions must behave the same as
 implicit conversions on delegates.
Okay, time to analyse this view. I'm assuming you mean that if you assign one delegate to another, the this pointer should be type checked the same way as visible parameter. In other words, the rhs delegate this pointer would be compatible with all values the lhs side is. This is how it works now. It certainly is important to check the delegate can actually accept it's context pointer, but it is *already done when the delegate is created*. For example, if you're creating a `void delegate(int) immutable`, you do it by taking address of a member function that has an `immutable` this pointer. The compiler will refuse this if the object you use to do it isn't `immutable`. This means that all delegates, when they are created (excluding type system bypassing tricks), can safely accept their own context, no matter how the context parameter is qualified. We can thus safely assign `void delegate(int) immutable` to `void delegate(int) const`, `void delegate(int)` or even `void delegate(int) shared`. The DIP has a simple rule for context parameter qualifiers when converting types: can always be removed but never added (` safe`ly). `immutable` counts as `immutable inout const shared`. So, the delegate context parameter qualifier is not needed to check the delegate will accept it's own context object when the delegate is in it's plain mutable unshared form. This is always the case. However, if the delegate is qualified somehow after it's creation, we still need to make sure the function it points to is still compatible with that before calling it. This is where the context parameter qualification is useful. If the context parameter is `immutable`, it is compatible with any qualification of the delegate. This is because an `immutable` context parameter means the object must also be immutable, and immutable objects are safe in both shared and read-only contextes. A `const` context parameter is also compatible with all qualifications. While the function behind the such a delegate might be `immutable` - which wouldn't support a mutable object - this can only be the case if the object is also immutable. `shared(void delegate(int) const)` might appear dangerous to call at first, but the DIP actually allows it. It is safe, because the DIP disallows converting `void delegate(int) const` one could obtain from a thread-local object to `shared(void delegate(int) const)`. Instead, it must originate from either `void delegate(int) immutable` or `void delegate(int) shared const`, and therefore the the object at time of delegate creation is also `immutable` or some variant of `shared`.
May 16
prev sibling parent reply Timon Gehr <timon.gehr gmx.ch> writes:
On 5/13/26 22:06, Walter Bright wrote:
 On 5/13/2026 12:32 AM, Dukc wrote:
 We can conclude that calling `const(void delegate(int))` is 
 fundamentally unsound. It can't be made to work without type system 
 breaking casts.
"Unsound" means it leads to corruption of the type system,
It does.
 such as being able to modify an immutable value. Unsound would mean something
like 2+2=5.
DMD v2.112.1 ```d import std; safe: class C{ int x=2; } Tuple!(int*,immutable(int)*) createBadAliasing(){ static foo()pure{ auto c = new C; auto dg = ()=>&c.x; return tuple(dg,c); } immutable t = foo(); return tuple(t[0](),&t[1].x); } void main(){ auto ps = createBadAliasing(); auto p = ps[0], q = ps[1]; static assert(is(typeof(p)==int*)); static assert(is(typeof(q)==immutable(int)*)); assert(p is q); auto x = *q; *p = 3; assert(x == 2); assert(x == *q); assert(2 + *q == 5); } ```
May 14
next sibling parent Timon Gehr <timon.gehr gmx.ch> writes:
On 5/14/26 23:55, Timon Gehr wrote:
 DMD v2.112.1
*v2.112.0 (typo, but likely v2.112.1 too once that releases)
May 14
prev sibling parent reply Walter Bright <newshound2 digitalmars.com> writes:
On 5/14/2026 2:55 PM, Timon Gehr wrote:
 ```d
 import std;
 
  safe:
 class C{ int x=2; }
 
 Tuple!(int*,immutable(int)*) createBadAliasing(){
      static foo()pure{
          auto c = new C;
          auto dg = ()=>&c.x;
      return tuple(dg,c);
      }
      immutable t = foo();
      return tuple(t[0](),&t[1].x);
 }
 
 void main(){
      auto ps = createBadAliasing();
      auto p = ps[0], q = ps[1];
      static assert(is(typeof(p)==int*));
      static assert(is(typeof(q)==immutable(int)*));
      assert(p is q);
      auto x = *q;
      *p = 3;
      assert(x == 2);
      assert(x == *q);
      assert(2 + *q == 5);
 }
 ```
Timon, I appreciate you stepping in to help. Your deductions are always insightful. But, sadly, I wish for examples that are as small as possible, not ones with templates, tuples, pure functions, static functions, nested functions, every module in Phobos, a class C that doesn't appear to serve any purpose, and other complications. I don't know which of the assertions should hold, and which ones are salient to your conclusion. I have a very small brain. Nearly every example of "I am confused by dip1000" is the result of overly complicated examples. Pare away *everything* that does not contribute to the problem, and then things come into focus. For example, the examples I have written in my comments in this thread.
May 14
parent reply Timon Gehr <timon.gehr gmx.ch> writes:
On 5/15/26 04:42, Walter Bright wrote:
 I don't know which of the assertions should hold, and which ones are 
 salient to your conclusion. I have a very small brain.
(There is enough context in my post to understand what is going on, so you can ask any competent LLM to explain it to you.) All assertions do pass, which you can confirm by running the example. (But ChatGPT understands that fact from just my post, without running it.) I cannot tell you which assertions should pass as the program should not be accepted by the compiler. Anyway, there are two groups of assertions: ```d static assert(is(typeof(p)==int*)); static assert(is(typeof(q)==immutable(int)*)); assert(p is q); ``` This proves that I have created aliasing that should be impossible to create in ` safe` code. ```d assert(x == 2); assert(x == *q); assert(2 + *q == 5); ``` This satisfies your 2+2==5 constraint. You said unsound is 2+2==5, this is literally 2+2==5. *q is the same as x, which is the same as 2. Anyway, here is the example cleaned up a bit, I did not have time to do this earlier as I had to leave in a hurry: ```d safe: struct T{ int* delegate() dg; int* q; } T foo()pure{ auto x = new int(2); auto dg = ()=>x; return T(dg,x); } void main(){ immutable ps = foo(); auto p = ps.dg(), q = ps.q; static assert(is(typeof(p)==int*)); static assert(is(typeof(q)==immutable(int*))); assert(p is q); auto x = *q; *p = 3; assert(x == 2); assert(x == *q); assert(2 + *q == 5); } ```
May 14
next sibling parent reply Meta <jared771 gmail.com> writes:
On Friday, 15 May 2026 at 03:13:05 UTC, Timon Gehr wrote:
 On 5/15/26 04:42, Walter Bright wrote:
 I don't know which of the assertions should hold, and which 
 ones are salient to your conclusion. I have a very small brain.
(There is enough context in my post to understand what is going on, so you can ask any competent LLM to explain it to you.)
I ran it through Gemini, and the explanation is excellent. LLMs are getting very good at understanding code: This D code demonstrates a subtle **type-system loophole** involving delegates, pure functions, and transitive immutability. By bypassing D's strict immutability guarantees, it creates a mutable alias to immutable memory, resulting in **Undefined Behavior (UB)**. This UB allows conflicting compiler optimizations to co-exist in the same block of code. ```d safe: struct T{ int* delegate() dg; int* q; } ``` * safe: ensures that the compiler enforces memory safety rules, preventing operations like raw pointer casting or uninitialized pointer access. * struct T aggregates a delegate dg (which returns a mutable int*) and a direct mutable pointer field q. ```d T foo() pure { auto x = new int(2); auto dg = ()=>x; return T(dg, x); } ``` * foo is marked pure, meaning it has no side effects and cannot access global mutable state. * auto x = new int(2); allocates an integer on the heap. * auto dg = ()=>x; creates a delegate capturing x. Because this delegate escapes the function scope via the return value, D automatically moves x into a heap-allocated closure context. * Because foo is strongly pure and returns a uniquely allocated, isolated object graph, D allows its return type to be implicitly converted to immutable. ```d immutable ps = foo(); auto p = ps.dg(), q = ps.q; ``` When ps is declared immutable, D attempts to apply transitive immutability to all its members: * ps.q becomes immutable(int*) (a pointer to an immutable integer). Thus, q has the type immutable(int*). * ps.dg becomes an immutable delegate instance. However, the *signature* of the delegate remains int* delegate(). When called, it evaluates its return type based on its original signature, yielding a **mutable** int*. This is the loophole: p receives a mutable pointer (int*) to the exact same underlying heap data that q views as immutable. ```d static assert(is(typeof(p) == int*)); static assert(is(typeof(q) == immutable(int*))); assert(p is q); ``` * The compile-time checks (static assert) confirm that p is mutable and q is immutable. * The runtime check assert(p is q); passes because both pointers hold the exact same memory address. ```d auto x = *q; // Reads 2 from memory *p = 3; // Mutates the memory location to 3 via the mutable pointer assert(x == 2); // Passes: x was copied before modification assert(x == *q); // Passes: The compiler optimizes based on immutability assert(2 + *q == 5); // Passes: The compiler emits a fresh load from memory ``` The execution of these final lines highlights how the optimizer exploits undefined behavior: 1. **assert(x == *q);**: Because q is explicitly typed as a pointer to immutable data, the D compiler's optimizer assumes that the value at *q can *never* change during execution. Instead of emitting an instruction to reload the value from RAM, it reuses the value (2) it already read during auto x = *q;. This evaluates to 2 == 2, which is true. 2. **assert(2 + *q == 5);**: In this mathematical expression, the optimizer fails to propagate the constant or decides to emit a fresh hardware memory load (often due to register allocation constraints). When the processor reads the physical address, it fetches the actual modified value (3) written by *p = 3. This evaluates to 2 + 3 == 5, which is also true. The code functions as a paradox because it exploits a flaw in D's type checking where delegate return types are not deeply qualified by the immutability of the parent instance. This allows safe code to trigger undefined behavior, causing the compiler to make contradictory optimization assumptions about the same memory address.
May 14
parent reply Walter Bright <newshound2 digitalmars.com> writes:
Thank you. It suggests to me like the problem is with the pure inheritance.

I suggest asking Gemini if it can reproduce the problem without a `pure`
annotation.

Or at least if it can craft a simpler example of the problem.
May 15
next sibling parent reply Richard (Rikki) Andrew Cattermole <richard cattermole.co.nz> writes:
On Saturday, 16 May 2026 at 02:02:42 UTC, Walter Bright wrote:
 Thank you. It suggests to me like the problem is with the pure 
 inheritance.

 I suggest asking Gemini if it can reproduce the problem without 
 a `pure` annotation.

 Or at least if it can craft a simpler example of the problem.
To agree with Walter here, if there are problems with pure please split them off into their own ticket. Please focus on const storage class & type qualifier in this proposal. Inferrable attributes that are modelling effects such as nothrow nogc pure scope and return are all on my list to be removed at a later date due to their inability to work correctly due to dmd's architecture. This is one of the main reasons (but not the only one) for the term "attribute soup" that is a bane of many D developer.
May 15
next sibling parent reply Walter Bright <newshound2 digitalmars.com> writes:
On 5/15/2026 7:45 PM, Richard (Rikki) Andrew Cattermole wrote:
 To agree with Walter here, if there are problems with pure please split them
off 
 into their own ticket.
Already done: https://github.com/dlang/dmd/issues/23125
 Please focus on const storage class & type qualifier in this proposal.
Since `pure` is part of the test case showing the problem, 23125 may be the actual cause of the problem.
 This is one of the main reasons (but not the only one) for the term 
"attribute soup" that is a bane of many D developer. I agree, but when one simplifies the problem, 9/10 times it is the result of misunderstanding the implicit `this` parameter. I recommend fixing 23125 first, and then seeing if the other problem is cleared up.
May 15
parent reply Timon Gehr <timon.gehr gmx.ch> writes:
On 5/16/26 06:35, Walter Bright wrote:
 On 5/15/2026 7:45 PM, Richard (Rikki) Andrew Cattermole wrote:
 To agree with Walter here, if there are problems with pure please 
 split them off into their own ticket.
Already done: https://github.com/dlang/dmd/issues/23125 ...
I approve, but it's indeed an unrelated problem.
 Please focus on const storage class & type qualifier in this proposal.
Since `pure` is part of the test case showing the problem, 23125 may be the actual cause of the problem. ...
No. It is not.
  > This is one of the main reasons (but not the only one) for the term 
 "attribute soup" that is a bane of many D developer.
 
 I agree, but when one simplifies the problem, 9/10 times it is the 
 result of misunderstanding the implicit `this` parameter.
 
 I recommend fixing 23125 first, and then seeing if the other problem is 
 cleared up.
It won't be.
May 15
parent Timon Gehr <timon.gehr gmx.ch> writes:
On 5/16/26 08:15, Timon Gehr wrote:
 On 5/16/26 06:35, Walter Bright wrote:
 On 5/15/2026 7:45 PM, Richard (Rikki) Andrew Cattermole wrote:
 To agree with Walter here, if there are problems with pure please 
 split them off into their own ticket.
Already done: https://github.com/dlang/dmd/issues/23125 ...
I approve, but it's indeed an unrelated problem.
I take that back, it's not even a problem. This bug report was correctly closed as invalid 14 hours ago. Implicit conversions of a delegate to `pure` is correctly disallowed: ```d class C{ void notPure(){ } } void main(){ void delegate()pure dg = &new C().notPure; // error } ``` ``` Error: cannot implicitly convert expression `&(new C).notPure` of type `void delegate()` to `void delegate() pure` ``` I.e., delegates don't even implicitly convert to `pure`. There is also not even a problem with inheritance: ```d class C{ void yesPure()pure{ } } class D:C{ override void yesPure(){ } } static assert(is(typeof(&new D().yesPure) == void delegate()pure)); ``` I.e., you made this bug up, it is not real. `D.yesPure` is `pure` because it inherits it when overriding the parent method.
May 16
prev sibling parent Timon Gehr <timon.gehr gmx.ch> writes:
On 5/16/26 04:45, Richard (Rikki) Andrew Cattermole wrote:
 On Saturday, 16 May 2026 at 02:02:42 UTC, Walter Bright wrote:
 Thank you. It suggests to me like the problem is with the pure 
 inheritance.

 I suggest asking Gemini if it can reproduce the problem without a 
 `pure` annotation.

 Or at least if it can craft a simpler example of the problem.
To agree with Walter here,
Walter is clearly just speculating, and his speculation is wrong.
 if there are problems with pure
No, there are problems with delegate context type checking. But type system features are not broken in isolation, soundness is a global property.
 please split  them off into their own ticket.
 ...
Rest assured my example is exactly in the right place in this thread.
 Please focus on const storage class & type qualifier in this proposal.
 ...
Please don't try to direct what we are allowed to talk about when you don't understand the technical details, otherwise you are just exacerbating the confusion. It is clear you have not read Dukc's DIP.
 Inferrable attributes that are modelling effects such as nothrow  nogc 
 pure scope and return are all on my list to be removed at a later date 
 due to their inability to work correctly due to dmd's architecture.
 
 This is one of the main reasons (but not the only one) for the term 
 "attribute soup" that is a bane of many D developer.
I don't understand why you are swooping in to ostensibly tell us to stay on topic when I am on topic and you are wildly off topic. x)
May 15
prev sibling next sibling parent reply Meta <jared771 gmail.com> writes:
On Saturday, 16 May 2026 at 02:02:42 UTC, Walter Bright wrote:
 Thank you. It suggests to me like the problem is with the pure 
 inheritance.

 I suggest asking Gemini if it can reproduce the problem without 
 a `pure` annotation.
No, as per the explanation I posted, it's an interaction between a pure factory function and what looks like a type system hole relating to delegate signatures. ps.dq is `immutable(int* delegate())`, and the immutable is for some reason not transitively applied to the delegate's return type. Thus ps.dg() returns int*, and ps.q returns immutable(int*), but both pointers point to the same object on the heap. It's not reproducible without the pure factory function, but the real issue lies with how immutable is transitively applied (or in this case, NOT applied) to delegate signatures. I guess the solution is that `immutable(int* delegate())` should become `int* delegate() immutable`.
May 15
parent reply Timon Gehr <timon.gehr gmx.ch> writes:
On 5/16/26 06:40, Meta wrote:
 I guess the solution is that `immutable(int* delegate())` should become 
 `int* delegate() immutable`.
No. Here is how D catches the same problem for classes: ```d safe class C{ int* x; this(int* x)pure{ this.x=x; } int* foo(){ return x; } } C foo()pure => new C(new int(2)); void main(){ immutable c = foo(); // ok c.foo(); // error } ``` ``` Error: mutable method `tt.C.foo` is not callable using a `immutable` object ``` However, delegates are actually not exactly the same case as classes. Calling `immutable(T delegate(S))` would be sound if there were no safe way to convert mutable references to `immutable` references. The existence of `pure` factory functions means that in this case delegates do have to behave like classes. It is a global interaction.
May 15
parent reply Meta <jared771 gmail.com> writes:
On Saturday, 16 May 2026 at 06:22:18 UTC, Timon Gehr wrote:
 On 5/16/26 06:40, Meta wrote:
 I guess the solution is that `immutable(int* delegate())` 
 should become `int* delegate() immutable`.
No. Here is how D catches the same problem for classes: ```d safe class C{ int* x; this(int* x)pure{ this.x=x; } int* foo(){ return x; } } C foo()pure => new C(new int(2)); void main(){ immutable c = foo(); // ok c.foo(); // error } ``` ``` Error: mutable method `tt.C.foo` is not callable using a `immutable` object ``` However, delegates are actually not exactly > the same case as classes.
Why not? All the way down to the bedrock of CS theory, these are equivalent cases.
 Calling `immutable(T delegate(S))` would be sound if there were 
 no safe way to convert mutable references to `immutable` 
 references.
Sure, but in D, a way does exist, so it's a moot point.
 The existence of `pure` factory functions means that in this 
 case delegates do have to behave like classes. It is a global 
 interaction.
Is your example not pretty much exactly equivalent to the fix I proposed? What are you disagreeing with me about?
May 15
parent reply Timon Gehr <timon.gehr gmx.ch> writes:
On 5/16/26 08:31, Meta wrote:
 On Saturday, 16 May 2026 at 06:22:18 UTC, Timon Gehr wrote:
 On 5/16/26 06:40, Meta wrote:
 I guess the solution is that `immutable(int* delegate())` should 
 become `int* delegate() immutable`.
No. Here is how D catches the same problem for classes: ```d safe class C{     int* x;     this(int* x)pure{ this.x=x; }     int* foo(){ return x; } } C foo()pure => new C(new int(2)); void main(){     immutable c = foo(); // ok     c.foo(); // error } ``` ``` Error: mutable method `tt.C.foo` is not callable using a `immutable` object ``` However, delegates are actually not exactly > the same case as classes.
Why not? All the way down to the bedrock of CS theory, these are equivalent cases. ...
I gave one difference in the next sentence, and your answer was "sure". It is a well-known property of equivalence that equivalent concepts have equivalent properties.
 Calling `immutable(T delegate(S))` would be sound if there were no 
 safe way to convert mutable references to `immutable` references.
Sure, but in D, a way does exist, so it's a moot point. ...
Just take `shared` then.
 The existence of `pure` factory functions means that in this case 
 delegates do have to behave like classes. It is a global interaction.
Is your example not pretty much exactly equivalent to the fix I proposed?
No. On the other hand if you had said `immutable(int* delegate())` should not be callable, but `immutable(int* delegate()immutable)` can keep being callable, then I would not have disagreed.
 What are you disagreeing with me about?
All the ways I am able to read the ambiguous sentence `immutable(int*) delegate())` should become `int* delegate() immutable`" are a wrong statement: - immutable(int* delegate()) should not decay to `int* delegate()immutable` either before or after fixing the type system. - `immutable(int* delegate())` should not be the same type as int* delegate()immutable either before or after fixing - `immutable(int* delegate())` before the fix should not have the same meaning as `int* delegate()immutable` after the fix Maybe you mean something else.
May 15
parent reply Meta <jared771 gmail.com> writes:
On Saturday, 16 May 2026 at 06:57:56 UTC, Timon Gehr wrote:
 On 5/16/26 08:31, Meta wrote:
 On Saturday, 16 May 2026 at 06:22:18 UTC, Timon Gehr wrote:
 On 5/16/26 06:40, Meta wrote:
 I guess the solution is that `immutable(int* delegate())` 
 should become `int* delegate() immutable`.
No. Here is how D catches the same problem for classes: ```d safe class C{     int* x;     this(int* x)pure{ this.x=x; }     int* foo(){ return x; } } C foo()pure => new C(new int(2)); void main(){     immutable c = foo(); // ok     c.foo(); // error } ``` ``` Error: mutable method `tt.C.foo` is not callable using a `immutable` object ``` However, delegates are actually not exactly > the same case as classes.
Why not? All the way down to the bedrock of CS theory, these are equivalent cases. ...
I gave one difference in the next sentence, and your answer was "sure".
Okay, then, *why* is calling immutable(int* delegate()) sound, but calling foo() on an immutable C is not? In both cases, you're calling a method that returns an immutable value but has an immutable context. I can't see how they're different.
 It is a well-known property of equivalence that equivalent 
 concepts have equivalent properties.

 Calling `immutable(T delegate(S))` would be sound if there 
 were no safe way to convert mutable references to `immutable` 
 references.
Sure, but in D, a way does exist, so it's a moot point. ...
Just take `shared` then.
 The existence of `pure` factory functions means that in this 
 case delegates do have to behave like classes. It is a global 
 interaction.
Is your example not pretty much exactly equivalent to the fix I proposed?
No. On the other hand if you had said `immutable(int* delegate())` should not be callable, but `immutable(int* delegate()immutable)` can keep being callable, then I would not have disagreed.
 What are you disagreeing with me about?
All the ways I am able to read the ambiguous sentence `immutable(int*) delegate())` should become `int* delegate() immutable`" are a wrong statement: - immutable(int* delegate()) should not decay to `int* delegate()immutable` either before or after fixing the type system. - `immutable(int* delegate())` should not be the same type as int* delegate()immutable either before or after fixing - `immutable(int* delegate())` before the fix should not have the same meaning as `int* delegate()immutable` after the fix Maybe you mean something else.
What I mean is this: ```d struct Delegate { void* funcptr; void* context; } ``` Currently immutable(int* delegate()) means that d.funcptr is immutable, but d.context is still mutable. So my suggestion is that it also make d.context immutable. This would disallow the code with UB that you outlined, because: struct T{    int* delegate() dg; int* q; } .. immutable ps = foo(); ps' type is immutable(T), so ps.dg is immutable(int* delegate()), which with my suggested change, "decays" to immutable(int* delegate() immutable), and now the compiler catches this because a delegate with an immutable context pointer is not allowed to access mutable data.
May 16
next sibling parent Dukc <ajieskola gmail.com> writes:
On Saturday, 16 May 2026 at 07:47:36 UTC, Meta wrote:
 Currently immutable(int* delegate()) means that d.funcptr is 
 immutable, but d.context is still mutable. So my suggestion is 
 that it also make d.context immutable. This would disallow the 
 code with UB that you outlined, because:
This was my original intention when I opened the thread in the ideas forum, and it is mentioned in the alternatives section of the DIP. Quirin Schroll and Nick Treleaven found it unworkable though. See especially [this post](https://forum.dlang.org/reply/egsorgodxgyswhzrnivf forum.dlang.org).
May 16
prev sibling parent Timon Gehr <timon.gehr gmx.ch> writes:
On 5/16/26 09:47, Meta wrote:
 
 What I mean is this:
 ```d
 struct Delegate
 {
      void* funcptr;
      void* context;
 }
 ```
 
 Currently immutable(int* delegate()) means that d.funcptr is immutable, 
 but d.context is still mutable. So my suggestion is that it also make 
 d.context immutable. This would disallow the code with UB that you 
 outlined, because:
 
 struct T{
      int* delegate() dg;
      int* q;
 }
 
 ..
 
 immutable ps = foo();
 
 ps' type is immutable(T), so ps.dg is immutable(int* delegate()), which 
 with my suggested change, "decays" to immutable(int* delegate() 
 immutable), and now the compiler catches this because a delegate with an 
 immutable context pointer is not allowed to access mutable data.
The conversion to `immutable` is allowed because `foo` is a `pure` factory function, not because the compiler assumes there is no qualified indirection in the delegate type. If you make `foo` not `pure`, the conversion is rejected.
May 16
prev sibling parent Timon Gehr <timon.gehr gmx.ch> writes:
On 5/16/26 04:02, Walter Bright wrote:
 Thank you. It suggests to me like the problem is with the pure inheritance.
 ...
What? No. It's an interaction with `pure` factory functions. Nothing at all to do with inheritance.
 I suggest asking Gemini if it can reproduce the problem without a `pure` 
 annotation.
 ...
I understand what is going on without asking Gemini.
 Or at least if it can craft a simpler example of the problem.
You specifically asked to modify `immutable` data, the conversion from mutable to `immutable` is needed for this. Violating `const` does not require a `pure` annotation, but violating `const` alone will likely not allow you to modify `immutable` data in this specific case.
May 15
prev sibling parent reply Walter Bright <newshound2 digitalmars.com> writes:
I've already acknowledged and filed a bug report with `pure` on delegates:

https://github.com/dlang/dmd/issues/23125

If the `pure` is necessary for your example issue, then that is the problem. If 
it is not necessary, why is it there?

P.S. Fixes to the compile go in the compiler test suite. Use of phobos is not 
allowed in the compiler test suite, as that is both unnecessary and makes it 
unduly difficult to develop the compiler.
May 15
parent Timon Gehr <timon.gehr gmx.ch> writes:
On 5/16/26 03:45, Walter Bright wrote:
 I've already acknowledged and filed a bug report with `pure` on delegates:
 
 https://github.com/dlang/dmd/issues/23125
 ...
This is a completely unrelated issue. Not every code that has a soundness issue and also contains the `pure` keyword is the same problem.
 If the `pure` is necessary for your example issue, then that is the 
 problem.
No, that's just a part in the exploit chain that allows me to modify `immutable` data, because that is what you asked for.
 If it is not necessary, why is it there?
 ...
To allow converting the result to `immutable`. The most obvious fix is to disallow calling a `immutable(T delegate(S))`, just like `const(T delegate(S))` must not be callable. We should at the very least be able to agree on this, because this is how it works for classes.
May 15
prev sibling parent reply FeepingCreature <feepingcreature gmail.com> writes:
On Tuesday, 12 May 2026 at 11:13:15 UTC, Dukc wrote:
 Now, there are some practically unavoidable corner cases where 
 such unsafe delegates can be created. Consider an abstract 
 class:

 ```D
 class A
 {   abstract  safe void foo() const;
 }
 ```

 If you convert an object of `A` to `const(A)`, this could be 
 problematic, because the concrete class of the object could be

 ```D
 class B : A
 {   int field;
      safe void delegate() del;
      safe void increment(){field++;}
      safe void foo() const {del();}
 }
 ```

 and `del` might point `increment` and context to the object 
 itself. The result would be that calling `foo` mutates the 
 object in violation of the `const` qualification.
I think this is actually an interesting case that is unrelated to the examples in the DIP? The question there is if the delegate belongs to the caller or the callee. For instance, nobody would argue that this code is a violation of constness: ``` class A { int x; int foo() const; } auto a = new A; if (a.foo() == 0) { a.x = 5; } ``` That is, because the caller holds a non-const reference to `a`, it is allowed to perform non-const operations on `a`. The question then is, if passing a delegate to a const method or handing it to a const class, makes the delegate "an effect of the method" if called, such that the delegate violates constness; or if it's semantically "an effect of the caller", such that the action is "performed by the caller, who holds a mutable reference." To me the most debatable case is ``` class A { int x; int foo(void delegate() dg) const { dg(); } } auto obj = new A; obj.x = 3; // we can unquestionably mutate obj auto dg = { obj.x = 4; }; // we can unquestionably wrap up the ability to mutate obj obj.foo(dg); // can we pass it to a method that is nominally const on obj? ``` I agree with the DIP, as far as I understand it, but this example does not seem like it is covered by it, and I don't think it should be either. Am I missing something?
May 12
parent reply FeepingCreature <feepingcreature gmail.com> writes:
On Wednesday, 13 May 2026 at 02:19:04 UTC, FeepingCreature wrote:
 I agree with the DIP, as far as I understand it, but this 
 example does not seem like it is covered by it, and I don't 
 think it should be either. Am I missing something?
Addendum: I mean, you can get into hot water much more easily: ``` safe: class C { int x; void foo() const { meep(); } } C c; void meep() { c.x ++; } void main() { auto c = new C; .c = c; c.foo; // c.x mutates as an effect of a const call! } ``` As far as I can tell this is totally unstoppable as global functions don't have const anyways. We'd need to consider the global scope as an implicit parameter so that it could be covered by constness even with global functions.
May 12
next sibling parent reply Walter Bright <newshound2 digitalmars.com> writes:
On 5/12/2026 7:31 PM, FeepingCreature wrote:
 ```
  safe:
 
 class C {
    int x;
    void foo() const { meep(); }
 }
 
 C c;
 void meep() { c.x ++; }
 
 void main() {
    auto c = new C;
    .c = c;
    c.foo; // c.x mutates as an effect of a const call!
 }
 ```
 
 As far as I can tell this is totally unstoppable as global functions don't
have 
 const anyways. We'd need to consider the global scope as an implicit parameter 
 so that it could be covered by constness even with global functions.
A const reference to something can also have a mutable reference to it at the same time. meep() is using a mutable path to c.
May 12
parent FeepingCreature <feepingcreature gmail.com> writes:
On Wednesday, 13 May 2026 at 04:41:37 UTC, Walter Bright wrote:
 On 5/12/2026 7:31 PM, FeepingCreature wrote:
 ```
  safe:
 
 class C {
    int x;
    void foo() const { meep(); }
 }
 
 C c;
 void meep() { c.x ++; }
 
 void main() {
    auto c = new C;
    .c = c;
    c.foo; // c.x mutates as an effect of a const call!
 }
 ```
 
 As far as I can tell this is totally unstoppable as global 
 functions don't have const anyways. We'd need to consider the 
 global scope as an implicit parameter so that it could be 
 covered by constness even with global functions.
A const reference to something can also have a mutable reference to it at the same time. meep() is using a mutable path to c.
yes that's what I'm saying, just because you're calling a const method doesn't mean you can be assured that the class you're calling it on won't be mutated as an effect on the call, only that it won't be mutated *via the passed context pointer.* that's why I think the example case given earlier with the delegate as a class field shouldn't be considered relevant.
May 12
prev sibling parent Dukc <ajieskola gmail.com> writes:
On Wednesday, 13 May 2026 at 02:31:43 UTC, FeepingCreature wrote:
 On Wednesday, 13 May 2026 at 02:19:04 UTC, FeepingCreature 
 wrote:

 Addendum: I mean, you can get into hot water much more easily:

 ```
  safe:

 class C {
   int x;
   void foo() const { meep(); }
 }

 C c;
 void meep() { c.x ++; }

 void main() {
   auto c = new C;
   .c = c;
   c.foo; // c.x mutates as an effect of a const call!
 }
 ```

 As far as I can tell this is totally unstoppable as global 
 functions don't have const anyways. We'd need to consider the 
 global scope as an implicit parameter so that it could be 
 covered by constness even with global functions.
Like Walter said, this is by design. `const` only says you can't mutate through it, not that you can't mutate it at all - `immutable` is for the latter meaning. So I guess you should add `pure` to both `foo` and `del` in the example, or alternatively use `immutable` in place of `const`, which would give the guarantee of no mutation from outside perspective. The issue still persists even if you do this, though.
May 13