digitalmars.dip.development - Uncallable delegates
- Dukc (5/5) Apr 01 I have (finally) finished my DIP draft for the [Delegates and
- Walter Bright (77/80) May 11 Thank you for your hard work on this. It's an important thing to get rig...
- Dukc (66/72) May 12 For context to anyone else following: the feedback phase has
- Dukc (3/4) May 12 Meant: because the fields of the delegate are not accessible from
- Walter Bright (108/108) May 12 I think of the issue completely differently. Allow me to try again, star...
- Dukc (85/88) May 13 So is that some delegates must be uncallable. I'll demonstrate,
- Walter Bright (20/23) May 13 "Unsound" means it leads to corruption of the type system, such as being...
- Dukc (14/33) May 13 Exactly. I think we're now talking about the same thing.
- Walter Bright (3/8) May 14 I'm glad we are understanding each other better.
- Timon Gehr (7/18) May 14 FWIW, I am convinced that Dukc has the right idea about delegate typing,...
- Walter Bright (3/8) May 14 I am skeptical that this proposed behavior does not match how overrides ...
- Timon Gehr (14/24) May 14 Why would the callability check match the overriding behavior?
- Walter Bright (3/5) May 15 The overriding of class functions must behave the same as implicit conve...
- Dukc (11/16) May 15 The DIP is proposing two things: changes to delegate qualifier
- Walter Bright (11/16) May 15 My concern is this problem is a problem with the implicit conversion of ...
- Timon Gehr (62/82) May 16 No, the example I gave you just relies on the result of a `pure`
- Walter Bright (2/2) May 18 Thank you. I will come back to this after I get the fixups working right...
- Timon Gehr (2/4) May 16 There is no such thing as an implicit conversion of a delegate to `pure`...
- Dukc (9/20) May 16 Like Timon wrote, there aren't actually any problems (AFAIK) with
- Timon Gehr (20/26) May 15 This is not correct. You can make it work that way and it would at least...
- Dukc (45/47) May 16 Okay, time to analyse this view.
- Timon Gehr (29/36) May 14 DMD v2.112.1
- Timon Gehr (2/3) May 14 *v2.112.0 (typo, but likely v2.112.1 too once that releases)
- Walter Bright (11/40) May 14 Timon, I appreciate you stepping in to help. Your deductions are always ...
- Timon Gehr (48/50) May 14 (There is enough context in my post to understand what is going on, so
- Meta (98/103) May 14 I ran it through Gemini, and the explanation is excellent. LLMs
- Walter Bright (3/3) May 15 Thank you. It suggests to me like the problem is with the pure inheritan...
- Richard (Rikki) Andrew Cattermole (11/16) May 15 To agree with Walter here, if there are problems with pure please
- Walter Bright (8/12) May 15 Since `pure` is part of the test case showing the problem, 23125 may be ...
- Timon Gehr (4/23) May 15 No. It is not.
- Timon Gehr (29/38) May 16 I take that back, it's not even a problem. This bug report was correctly...
- Timon Gehr (10/31) May 15 No, there are problems with delegate context type checking. But type
- Meta (13/17) May 15 No, as per the explanation I posted, it's an interaction between
- Timon Gehr (24/26) May 15 No.
- Meta (6/36) May 15 Why not? All the way down to the bedrock of CS theory, these are
- Timon Gehr (18/65) May 15 I gave one difference in the next sentence, and your answer was "sure".
- Meta (28/99) May 16 Okay, then, *why* is calling immutable(int* delegate()) sound,
- Dukc (7/11) May 16 This was my original intention when I opened the thread in the
- Timon Gehr (5/33) May 16 The conversion to `immutable` is allowed because `foo` is a `pure`
- Timon Gehr (10/16) May 15 What? No.
- Walter Bright (7/7) May 15 I've already acknowledged and filed a bug report with `pure` on delegate...
- Timon Gehr (10/18) May 15 This is a completely unrelated issue. Not every code that has a
- FeepingCreature (39/60) May 12 I think this is actually an interesting case that is unrelated to
- FeepingCreature (20/23) May 12 Addendum: I mean, you can get into hot water much more easily:
- Walter Bright (3/24) May 12 A const reference to something can also have a mutable reference to it a...
- FeepingCreature (7/33) May 12 yes that's what I'm saying, just because you're calling a const
- Dukc (8/29) May 13 Like Walter said, this is by design. `const` only says you can't
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
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
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
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
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
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
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
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.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.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.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
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
On 5/14/26 21:34, Walter Bright wrote:On 5/13/2026 1:30 PM, Dukc 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.)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
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
On 5/15/26 04:23, Walter Bright wrote:On 5/14/2026 2:55 PM, Timon Gehr wrote: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?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
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
On Saturday, 16 May 2026 at 02:05:17 UTC, Walter Bright wrote:On 5/14/2026 7:36 PM, Timon Gehr wrote: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?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
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
On 5/16/26 08:47, Walter Bright wrote:On 5/15/2026 9:55 PM, Dukc wrote:There is no implicit conversion of a delegate to `pure` in the example.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. ...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
Thank you. I will come back to this after I get the fixups working right on the AArch64 code gen.
May 18
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
On Saturday, 16 May 2026 at 06:47:21 UTC, Walter Bright wrote:On 5/15/2026 9:55 PM, Dukc wrote: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.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.
May 16
On 5/16/26 04:05, Walter Bright wrote:On 5/14/2026 7:36 PM, Timon Gehr wrote: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.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
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
On 5/13/26 22:06, Walter Bright wrote:On 5/13/2026 12:32 AM, Dukc wrote:It does.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.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
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
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
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
On Friday, 15 May 2026 at 03:13:05 UTC, Timon Gehr wrote:On 5/15/26 04:42, Walter Bright wrote: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.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.)
May 14
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
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
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/23125Please 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
On 5/16/26 06:35, Walter Bright wrote:On 5/15/2026 7:45 PM, Richard (Rikki) Andrew Cattermole wrote:I approve, but it's indeed an unrelated problem.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 ...No. It is not.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.It won't be.
May 15
On 5/16/26 08:15, Timon Gehr wrote:On 5/16/26 06:35, Walter Bright wrote: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.On 5/15/2026 7:45 PM, Richard (Rikki) Andrew Cattermole wrote:I approve, but it's indeed an unrelated problem.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 ...
May 16
On 5/16/26 04:45, Richard (Rikki) Andrew Cattermole wrote:On Saturday, 16 May 2026 at 02:02:42 UTC, Walter Bright wrote:Walter is clearly just speculating, and his speculation is wrong.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 pureNo, 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
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
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
On Saturday, 16 May 2026 at 06:22:18 UTC, Timon Gehr wrote:On 5/16/26 06:40, Meta wrote:Why not? All the way down to the bedrock of CS theory, these are equivalent cases.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.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
On 5/16/26 08:31, Meta wrote:On Saturday, 16 May 2026 at 06:22:18 UTC, Timon Gehr wrote: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.On 5/16/26 06:40, Meta wrote:Why not? All the way down to the bedrock of CS theory, these are equivalent cases. ...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.Just take `shared` then.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. ...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.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?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
On Saturday, 16 May 2026 at 06:57:56 UTC, Timon Gehr wrote:On 5/16/26 08:31, Meta wrote: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.On Saturday, 16 May 2026 at 06:22:18 UTC, Timon Gehr wrote:I gave one difference in the next sentence, and your answer was "sure".On 5/16/26 06:40, Meta wrote:Why not? All the way down to the bedrock of CS theory, these are equivalent cases. ...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.It is a well-known property of equivalence that equivalent concepts have equivalent properties.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.Just take `shared` then.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. ...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.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?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 16
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
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
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
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
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
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
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
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
On Wednesday, 13 May 2026 at 04:41:37 UTC, Walter Bright wrote:On 5/12/2026 7:31 PM, FeepingCreature wrote: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.``` 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
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









Dukc <ajieskola gmail.com> 