digitalmars.dip.ideas - Delegates and qualifier transitivity
- Dukc (64/64) Dec 05 2025 D type qualifiers - `const`, `immutable`, `shared` and `inout` -
- Dukc (6/8) Dec 08 2025 Duh, there is a hole in my proposal as [Timon noted in a DMD
- Dukc (5/7) Dec 24 2025 Update: I have not given up. I have spent time reading the spec
- Dukc (34/36) Dec 29 2025 Well, investigation finally done. Fortunately I don't think the
- Quirin Schroll (81/119) Jan 15 First, let me say that this is a tricky area and even I struggle
- Dukc (50/90) Jan 16 It definitely involves thinking really hard and slow for me.
- Quirin Schroll (150/243) Feb 04 I’m not so much concerned how the language currently works
- Dukc (23/58) Feb 09 The spec does not describe how it works and it was difficult for
- Quirin Schroll (79/108) Feb 17 If I understand you correctly, for every qualifier `q`, you want
- Nick Treleaven (41/53) Feb 18 OK. Also without `pure`. So presumably a top-level attribute on a
- Richard (Rikki) Andrew Cattermole (10/16) Feb 18 The more I think about this, the more I suspect that the language isn't
- Dukc (11/18) Feb 18 Ouch, that's bad! Maybe it also means that `void[]` can't be
- Dukc (9/24) Feb 23 I have decided that the non-transitive system (as I define it
- Quirin Schroll (13/66) Feb 19 Yes, it’s not relevant and represents arbitrary attributes.
- Dukc (18/28) Feb 18 Assuming you meant `const(shared int)` is the same as
- Quirin Schroll (91/120) Feb 19 Yeah, kind of. I was after “There are many fundamentally
- Dukc (6/16) Dec 29 2025 I was mistaken on this point. In my example, `imm` is typed as
- Dukc (5/13) Dec 29 2025 I still think this point stands, but changing it isn't necessary
- Dukc (4/4) Feb 16 Started to write the actual DIP now.
- Timon Gehr (823/829) Feb 22 Sorry, I mean to respond to this initially, but was too busy at the time...
- Timon Gehr (6/7) Feb 22 (Note that this does not take into account other restrictions on
- Dukc (7/19) Feb 23 Haha, finally it pays off that I've dabbled with Haskell, so I
- Dukc (13/13) Mar 18 DIP draft progress report:
- Dukc (4/5) Mar 18 Meant either: the 14th week, or the week after the next. I
- Walter Bright (44/44) Mar 05 Thank you for taking on this tricky problem. Trying to figure it out wil...
- Walter Bright (2/2) Mar 05 The spec does briefly cover covariance, but not contravariance:
- Walter Bright (6/6) Mar 05 An explanation of Covariance and Contravariance:
- Nick Treleaven (2/4) Mar 07 https://github.com/dlang/dlang.org/pull/4415
- Dukc (65/82) Mar 06 This part is correct and already works as it should, as far as I
- Walter Bright (11/14) Mar 06 Yes, please describe the behavior in terms of covariance and contravaria...
- Dukc (16/21) Mar 06 `pure` is there to allow those optimisations (in the spec sense,
- Paul Backus (6/18) Mar 06 `@system` code can change the `.ptr` and `.funcptr` properties of
- Dukc (25/47) Mar 07 You're thinking something like
- Timon Gehr (7/25) Mar 07 Currently, the only valid way `del` can be typed as `R delegate()const`
- Timon Gehr (2/4) Mar 07
- Paul Backus (23/39) Mar 07 No, I'm thinking of code that knows via out-of-band information
- Timon Gehr (8/55) Mar 07 While this DIP will have breaking changes, I don't expect it to propose
- Dukc (16/58) Mar 07 Although I said the langauge would assume you don't do this, I
- Timon Gehr (4/23) Mar 07 This is completely backwards. Whatever the assumptions that can be
- Paul Backus (7/22) Mar 07 According to the spec, these are the assumptions that `@safe`
- Timon Gehr (13/40) Mar 07 The text you quoted literally is saying the `.ptr` property must refer
- Timon Gehr (5/9) Mar 07 This is not true. Delegates are not simply a pair. The delegate type
D type qualifiers - `const`, `immutable`, `shared` and `inout` -
are supposed to be transitive.
Delegates are supposed to work like structs that contain a
function pointer and an untyped context pointer. In my opinion at
least, this means the context pointer should have the same
qualifiers as the delegate itself. In other words,
`immutable(void delegate())` should be the same as
`immutable(void delegate() immutable)` (it's still fine if the
return type is mutable.).
Currently the compiler seems so buggy in this regard that I'm not
sure what it tries to do. While testing, I even managed to get a
declaration like `immutable del = &immutableStruct.immutableFun;`
to have the compiler say "Error: cannot implicitly convert
expression `&immutableStruct.immutableFun` of type `int
delegate() immutable safe` to `immutable(int delegate() safe)`"
Which leads to implicit conversions between delegates being wrong:
```D
struct S
{ int field;
safe mutableFun(){}
safe constFun() const{}
safe immutableFun() immutable{}
}
safe void main()
{ void delegate() safe mut;
void delegate() safe const con;
void delegate() safe immutable imm;
S s1;
immutable S s2;
mut = &s1.mutableFun; //Compiles, as it should
mut = &s1.constFun; //Compiles as it should
mut = &s2.immutableFun; //Doesn't compile but should
con = &s1.mutableFun; //Doesn't compile and neither should
con = &s1.constFun; //Compiles as it should
con = &s2.immutableFun; //Doesn't compile but should
imm = &s1.mutableFun; //Doesn't compile and neither should
imm = &s1.constFun; //Compiles but shouldn't
imm = &s2.immutableFun; //Compiles, as it should
}
```
You might be wondering why `mut = &s2.immutableFun;` should be
allowed in safe code - a context pointer to mutable even when the
struct is immutable. Well, the pointer is typed as `void*`. You
can't mutate through that in ` safe` code, and calling the
delegate will not do so either since the referred function
actually takes the context pointer as immutable. The compiler
guarantees it does, as it rejects delegates like `&s2.mutableFun`.
On the other hand, `imm = &s1.constFun;` is dangerous. While
`constFun` itself wont mutate the struct, immutability assumes
no-one will mutate the data. The compiler is free to assume the
context of the delegate will remain untouched by other things
done in the calling function, yet clearly the assumption could be
easily broken by mutating `s1`, being a mutable struct. Also, an
immutable delegate could be stored to thread-shared memory, and
the results of calling it would depend on a thread-local context
- the very issue `immutable` and `shared` are supposed to prevent.
Changing the behaviour will in all likelihood lead to fairly
large breakage, which means it must be done over an edition
switch. But first, before I go on to write an actual DIP (and
maybe the proposed compiler changes also), I'd like to ask you to
check my assessment. Is there a reason other than just plain
misdesign why it works how it works right now? If not, the fix it
pictured above seems clear to me, but is it really? Is there
another way to solve this that should be considered?
Dec 05 2025
On Friday, 5 December 2025 at 16:37:35 UTC, Dukc wrote:If not, the fix it pictured above seems clear to me, but is it really?Duh, there is a hole in my proposal as [Timon noted in a DMD issue suggesting part of what I did](https://github.com/dlang/dmd/issues/19131#issuecomment-2541990651). Yet the way how it works now is also still clearly wrong. I have to rethink this. I suspect the solution will have to be quite hairy to be sound. Ideas, anyone?
Dec 08 2025
On Monday, 8 December 2025 at 20:55:29 UTC, Dukc wrote:I have to rethink this. I suspect the solution will have to be quite hairy to be sound. Ideas, anyone?Update: I have not given up. I have spent time reading the spec and DMD source, but had too much else to do lately. I should get this proposal fixed within Christmastide, probably within this year.
Dec 24 2025
On Monday, 8 December 2025 at 20:55:29 UTC, Dukc wrote:I have to rethink this. I suspect the solution will have to be quite hairy to be sound. Ideas, anyone?Well, investigation finally done. Fortunately I don't think the solution is going to be as hairy as I feared, if it gets accepted. First, I should clarify how the compiler currently thinks about delegate qualifiers as this isn't written in the spec or anything, and unless I was mistaken in my initial testing there are also bugs that further confound what's the intended scheme. It does not think about them transitively. `qualifier(void delegate())` and `qualifier(void delegate() qualifier)` are treated as different types. When considering whether a delegate `T delegate() qualifierA` can be assigned to `T delegate() qualifierB`, the answer is the the same as whether `T function(qualifierA void*)` can be assigned to `T function(qualifierB void*)`. Meaning, `void delegate() const` can be assigned to `void delegate()`, but not vice-versa. What about qualifiers to the whole delegate? Converting between them is not restricted. At all. You can convert between `yourDelegate`, `immutable(yourDelegate)`, `const(yourDelegate)`, `inout(yourDelegate)` and even `shared(yourDelegate)` just as freely as if you replaced `yourDelegate` with `int`. Obviously, this breaks the type system assumptions about qualifier transitivity hard. What to do about this? I still the delegate qualifiers should also apply to the `this` argument. You probably don't want a delegate with an `immutable` context that can't actually be called with one, and if you really do you can accomplish that manually with a function pointer/context pointer pair. Instead, qualifier conversions between delegates should be restricted in ` safe` code. You would be able to safely cast `qualifier(T delegate(args))` and `T delegate(args) qualifier` to each other, but just like you can't cast `immutable(int**)` to `int**` you couldn't cast `immutable int delegate()` to `int delegate`. This also needs to apply to pure factory function casts of mutables to `const` or `immutable`.
Dec 29 2025
First, let me say that this is a tricky area and even I struggle at times with it despite having a mathematics degree. It seemed Timon Gehr and I were the only ones who cared about getting this problem solved. On Monday, 29 December 2025 at 17:51:46 UTC, Dukc wrote:On Monday, 8 December 2025 at 20:55:29 UTC, Dukc wrote:This is not the right analysis. A delegate is *implemented* very similar to a struct with a `void*` context and a function pointer, but that doesn’t suffice for what you’re trying. A delegate also has an important invariant: The function pointer and the context match, i.e. it is valid to call the function pointer “on” the context. That is checked (ideally) statically when creating a delegate. Thus, the components of a delegate cannot be assigned individually in ` safe` code. For that reason, all qualifiers, including `immutable` and `shared`, as member function attributes on delegate types, only provide outward guarantees and don’t make inward demands after the delegate is formed. **That leads to the following diagram:** ``` immutable (←) function ↓ const inout shared → const shared / inout shared → shared ↓ ↓ ↓ ↓ const inout → const / inout → (mutable) ``` **The rules:** 1. `immutable` converts to anything. 2. everything converts to *mutable.* 3. `const`, `shared`, and `inout` can be removed at will. 4. `const`, `shared`, and `inout` cannot be added. In the diagram, `/` means that there are two separate diagrams, one with the left-hand sides and one with the right-hand sides. The conversion indicated by `(←)` means that it’s not a reference conversion, but a value conversion, very much like `int` to `long`. **The rule for delegate formation:** If the qualifier member function attributes are `quals`, for all captured variables `var`, the assignment `ref quals(typeof(var)) _ = var` must be valid. Inference for lambdas etc. should attempt to apply all viable member function attributes (`immutable`, `const`, `shared`, and, in an `inout` context, also `inout`), very much like `pure`, `nothrow`, ` nogc`, and ` safe`. It might be worth adding the keyword `function` as an attribute for delegate types specifically to encode the fact that there is no context at all.I have to rethink this. I suspect the solution will have to be quite hairy to be sound. Ideas, anyone?Well, investigation finally done. Fortunately I don't think the solution is going to be as hairy as I feared, if it gets accepted. First, I should clarify how the compiler currently thinks about delegate qualifiers as this isn't written in the spec or anything, and unless I was mistaken in my initial testing there are also bugs that further confound what's the intended scheme. It does not think about them transitively. `qualifier(void delegate())` and `qualifier(void delegate() qualifier)` are treated as different types. When considering whether a delegate `T delegate() qualifierA` can be assigned to `T delegate() qualifierB`, the answer is the the same as whether `T function(qualifierA void*)` can be assigned to `T function(qualifierB void*)`. Meaning, `void delegate() const` can be assigned to `void delegate()`, but not vice-versa.What about qualifiers to the whole delegate? Converting between them is not restricted. At all. You can convert between `yourDelegate`, `immutable(yourDelegate)`, `const(yourDelegate)`, `inout(yourDelegate)` and even `shared(yourDelegate)` just as freely as if you replaced `yourDelegate` with `int`. Obviously, this breaks the type system assumptions about qualifier transitivity hard. What to do about this? I still the delegate qualifiers should also apply to the `this` argument. You probably don't want a delegate with an `immutable` context that can't actually be called with one, and if you really do you can accomplish that manually with a function pointer/context pointer pair. Instead, qualifier conversions between delegates should be restricted in ` safe` code. You would be able to safely cast `qualifier(T delegate(args))` and `T delegate(args) qualifier` to each other, but just like you can't cast `immutable(int**)` to `int**` you couldn't cast `immutable int delegate()` to `int delegate`. This also needs to apply to pure factory function casts of mutables to `const` or `immutable`.Now, what about outside qualifiers? They can change what the object can do. A delegate can only be invoked, so the question is: When can a qualified delegate type be invoked? Here, `immutable` equals `immutable const inout shared`. **The rule:** The delegate type `q₁(R delegate(…) q₂)` can be invoked if: 1. `q₁` is a subset of `q₂`, or 2. `q₂` includes `const` and `shared`. Clause 1 expresses that the function pointer guarantees all that is asked of it. Clause 2 works because if the delegate guarantees it’s not going to incur changes through the context (it’s `const`) and guarantees that it’s doing it in a thread-safe manner, the invocation is valid. What about `immutable(int delegate() const)`? It doesn’t match Clause 1 or Clause 2, so it’s not valid to invoke. That is because `immutable` is implicitly `shared`. That would allow passing a callable delegate to different threads that could invoke it concurrently despite it not supporting that. Outside qualifiers don’t matter for `pure` factory delegates at all. Only `R delegate(…) pure immutable` provides enough guarantees to make a unique object, and it can always be invoked, no matter what outside qualifiers you apply to it. The next weaker qualification is `pure const inout shared` and it’s not enough: `inout` makes no guarantees (it could be *mutable*), and `const` plus `shared` isn’t enough as mutable indirections might exist. Also, why shouldn’t you be able to convert an `immutable(R delegate(…) const shared)` to `shared(R delegate(…) const shared)`? By copy, that is. An obvious principle is: If a delegate type can’t be invoked, it definitely can’t be converted to something that can be invoked. While a delegate is a reference type, removing `inout`/`immutable` from the context is fine while retaining `const` and `shared` because the context is opaque and the function pointer is tightly coupled.
Jan 15
On Thursday, 15 January 2026 at 23:53:20 UTC, Quirin Schroll wrote:First, let me say that this is a tricky area and even I struggle at times with it despite having a mathematics degree. It seemed Timon Gehr and I were the only ones who cared about getting this problem solved.It definitely involves thinking really hard and slow for me. Thanks for reading despite the difficulty!This is not the right analysis. A delegate is *implemented* very similar to a struct with a `void*` context and a function pointer, but that doesn’t suffice for what you’re trying. A delegate also has an important invariant: The function pointer and the context match, i.e. it is valid to call the function pointer “on” the context. That is checked (ideally) statically when creating a delegate. Thus, the components of a delegate cannot be assigned individually in ` safe` code.Here, I was describing how the language *currently* works. I agree the invariant you write about should hold at all times but currently, it does not.For that reason, all qualifiers, including `immutable` and `shared`, as member function attributes on delegate types, only provide outward guarantees and don’t make inward demands after the delegate is formed.That right, and I don't think that parts needs to change. It is why `void delegate() const` can be assigned to `void delegate()`. `void delegate()` does not have to actually be able to mutate it's parameter, it just needs to work with variables that might be mutated by others, just like `void delegate() const`.**The rules:** 1. `immutable` converts to anything. 2. everything converts to *mutable.* 3. `const`, `shared`, and `inout` can be removed at will. 4. `const`, `shared`, and `inout` cannot be added.Yes, I agree with allowing all of these as far as I see. I was just writing that I'd be more liberal I don't see an issue converting `R delegate() const` to `R delegate immutable`, which is also allowed right now, because the context pointer isn't immutable so you don't end up actually breaking the assumption of the function you're pointing to. But I just realised I also proposed that `R delegate immutable` and `immutable(R delegate)` would freely convert to each other, which means this would be possible: ```D S var; void delegate() immutable del1 = &var.constMemFun; immutable void delegate del2 = del1; // context of del2 modified -> undefined behaviour var.field++; ``` So I guess we need to stick with what you rule here, although I'll need to think this over once more another day before I feel I have an informed opinion.Inference for lambdas etc. should attempt to apply all viable member function attributes (`immutable`, `const`, `shared`, and, in an `inout` context, also `inout`), very much like `pure`, `nothrow`, ` nogc`, and ` safe`.So also for member functions that have auto-inference on I presume. I'm not sure this is a good idea, or at least not when bundled with the delegate qualifier fix DIP we're discussing. Auto inference is a separate language feature after all.It might be worth adding the keyword `function` as an attribute for delegate types specifically to encode the fact that there is no context at all.Do you mean that we could write `int function(int) safe` alternatively as `int delegate(int) safe function`? I think this is also off scope for the DIP I'm thinking.**The rule:** The delegate type `q₁(R delegate(…) q₂)` can be invoked if: 1. `q₁` is a subset of `q₂`, or 2. `q₂` includes `const` and `shared`.This would be slightly more powerful than what I'm proposing, but also quite a bit more complex and annoying to type, since you couldn't shorten `immutable R delegate() immutable` to `immutable R delegate()`. Would the fact your `immutable(R delegate() const shared)` needs to be `const shared(R delegate())` instead be such a major issue that the complications are worth it?Outside qualifiers don’t matter for `pure` factory delegates at all. Only `R delegate(…) pure immutable` provides enough guarantees to make a unique object, and it can always be invoked, no matter what outside qualifiers you apply to it. The next weaker qualification is `pure const inout shared` and it’s not enough: `inout` makes no guarantees (it could be *mutable*), and `const` plus `shared` isn’t enough as mutable indirections might exist.The issue is not using the delegate itself as the pure factory function. The issue is that a mutable delegate with a mutable context could be *returned* from a pure factory function and then converted to an immutable delegate with an "immutable" context. Check [the issue](https://github.com/dlang/dmd/issues/19131#issuecomment-2541990651) and you'll understand.
Jan 16
On Friday, 16 January 2026 at 19:45:28 UTC, Dukc wrote:On Thursday, 15 January 2026 at 23:53:20 UTC, Quirin Schroll wrote:I want this kind of feature/fix.First, let me say that this is a tricky area and even I struggle at times with it despite having a mathematics degree. It seemed Timon Gehr and I were the only ones who cared about getting this problem solved.It definitely involves thinking really hard and slow for me. Thanks for reading despite the difficulty!I’m not so much concerned how the language currently works because it’s broken. I trust you that you’re perfectly able to describe that for the DIP. What I tried to achieve with my answer is to help you (and other people reading it) see where you ought to strive for, mostly so we don’t end up in a bad spot.This is not the right analysis. A delegate is *implemented* very similar to a struct with a `void*` context and a function pointer, but that doesn’t suffice for what you’re trying. A delegate also has an important invariant: The function pointer and the context match, i.e. it is valid to call the function pointer “on” the context. That is checked (ideally) statically when creating a delegate. Thus, the components of a delegate cannot be assigned individually in ` safe` code.Here, I was describing how the language *currently* works. I agree the invariant you write about should hold at all times but currently, it does not.You do break the assumption because the member function attribute `immutable` on a delegate means “the context cannot change” and not “the context will not be changed by a call to this”; the implication is that a `int* delegate() immutable pure` returns unique objects that can be converted to `immutable`, but a `int* delegate() const pure` doesn’t provide enough guarantees for that.For that reason, all qualifiers, including `immutable` and `shared`, as member function attributes on delegate types, only provide outward guarantees and don’t make inward demands after the delegate is formed.That right, and I don't think that parts needs to change. It is why `void delegate() const` can be assigned to `void delegate()`. `void delegate()` does not have to actually be able to mutate it's parameter, it just needs to work with variables that might be mutated by others, just like `void delegate() const`.**The rules:** 1. `immutable` converts to anything. 2. everything converts to *mutable.* 3. `const`, `shared`, and `inout` can be removed at will. 4. `const`, `shared`, and `inout` cannot be added.Yes, I agree with allowing all of these as far as I see. I was just writing that I'd be more liberal I don't see an issue converting `R delegate() const` to `R delegate immutable`, which is also allowed right now, because the context pointer isn't immutable so you don't end up actually breaking the assumption of the function you're pointing to.But I just realised I also proposed that `R delegate immutable` and `immutable(R delegate)` would freely convert to each other, which means this would be possible: ```D S var; void delegate() immutable del1 = &var.constMemFun; immutable void delegate del2 = del1; // context of del2 modified -> undefined behaviour var.field++; ``` So I guess we need to stick with what you rule here, although I'll need to think this over once more another day before I feel I have an informed opinion.I derived the rule from what *must* be true to keep the guarantees `immutable` and `const` make. Especially `immutable` is great to consider because the rule is: If it changes, your system is broken; that’s rather easy to figure out. I’ve tried to be as liberal as possible without breaking the guarantees of the type system.That would be a separate proposal, and you’re mistaken a bit. The only ones that can be inferred are `const` and `inout`, because those make an outward guarantee and inward demands. An `auto` member function can’t infer `immutable` because that one also makes an outward demand: it can only be called on `immutable` objects; we don’t know what objects the member function is going to be called on. If you want that kind of inference, you can ask for it using a template `this` parameter. The same is true with `shared` because, like `immutable`, it is incompatible with the default (mutable). The case for a lambda is different because the qualifiers of the context (i.e. the object) are statically known when forming the delegate, thus we can infer `immutable` if everything the context refers to is immutable or unique. ```d void printDG(scope int* delegate() immutable pure safe callback) safe { import std.stdio; immutable int* p = callback(); // requires immutable pure writeln(*p); } void main() safe { import std.stdio; int sum = 0; int x, y; readf!" %s %s"(x, y); sum = x + y; auto average = () => sum / 2; printDG(average); } ``` The type of `average` can be inferred to `int delegate() immutable pure safe …` because the context is unique: `sum` isn’t immutable, but it isn’t being mutated after the lambda is accessible. The lambda doesn’t mutate it, so it’s `int delegate() const …`, but on a closer look, the compiler can figure out that the context, after being established, isn’t going to change. In fact, if we wanted to be maximally permissive, we could even allow changes to the context again if the lambda isn’t accessible (or actually accessed) anymore: ```d void printDG(scope int delegate() immutable safe callback) safe; void main() safe { import std.stdio; int sum = 0; int x, y; readf!" %s %s"(x, y); sum = x + y; { auto average = () => sum / 2; printDG(average); } sum += 1; } ``` That is, if the context is provably not changing for the entire lifetime of the lambda, mind you `scope` on `printDG` is essential to prove this, it’s okay to mutate the context however we see fit. We could even allow changes to the context after the lambda has been formed if it cannot have been called yet: ```d void printDG(scope int delegate() immutable safe callback) safe; void main() safe { import std.stdio; int sum = 0; int x, y; readf!" %s %s"(x, y); auto average = () => sum / 2; sum = x + y; printDG(average); sum += 1; } ``` That’s because the change is not observable. Since variables in a context are references (not copies), the ordering of `auto average = () => sum / 2;` and `sum = x + y;` is inessential. Of course, the more you allow, the more complex the compiler gets and it becomes more difficult to explain why something doesn’t work here but something similar works there.Inference for lambdas etc. should attempt to apply all viable member function attributes (`immutable`, `const`, `shared`, and, in an `inout` context, also `inout`), very much like `pure`, `nothrow`, ` nogc`, and ` safe`.So also for member functions that have auto-inference on I presume. I'm not sure this is a good idea, or at least not when bundled with the delegate qualifier fix DIP we're discussing. Auto inference is a separate language feature after all.My idea is that in `int delegate(int) safe function`, the `function` encodes that the context is guaranteed to be unused (and `null`).It might be worth adding the keyword `function` as an attribute for delegate types specifically to encode the fact that there is no context at all.Do you mean that we could write `int function(int) safe` alternatively as `int delegate(int) safe function`? I think this is also off scope for the DIP I'm thinking.Thinking about it, I’m not sure you can even safely get to a non-null `immutable R delegate()` that’s not originating from a `R delegate() immutable`, so this might work, coincidentally. (By “coincidentally” I mean it doesn’t translate to other qualifiers.) If you really want to, you can shorten `immutable(R delegate() immutable)` to `immutable(R delegate())`. Unfortunately, same isn’t true for `const(R delegate())` because you should definitely be able to bind any (mutable) object of type `T` to a `const ref T`, including the case where `T` is any delegate type. If delegates break that, they’d become very special types and super-annoying to deal with. The fact that you can’t call a `const(R delegate())` might annoy people, but the transitivity of `const` simply requires it.**The rule:** The delegate type `q₁(R delegate(…) q₂)` can be invoked if: 1. `q₁` is a subset of `q₂`, or 2. `q₂` includes `const` and `shared`.This would be slightly more powerful than what I'm proposing, but also quite a bit more complex and annoying to type, since you couldn't shorten `immutable R delegate() immutable` to `immutable R delegate()`.Would the fact your `immutable(R delegate() const shared)` needs to be `const shared(R delegate())` instead be such a major issue that the complications are worth it?I’m not sure I understand you correctly. An `immutable(R delegate() const shared)` fall into the same category as the `immutable(R delegate())`: If I’m right, it can only originate from a `R delegate() immutable`, and thus it’s fine, more or less coincidentally. Outside qualifications and member function attributes are very different things (and while they look the same, member function attributes on delegates don’t mean the same as they do on member functions). The member function attributes encode information about the context and what the function might do with it and what it won’t do with it. An outside qualification only imposes restrictions on the type and all operations must respect it or be banned. An outside `const` for a delegate means: The context cannot change by actions done by this delegate. If an action of the object (i.e. the call operator) cannot guarantee that, it must be banned.Thanks for pointing me to this, I totally didn’t think about uniqueness in that regard. You have a `immutable(R delegate())` and those can’t be called given the initial analysis (especially when viewed as a `const(R delegate())`). On the other hand (cf. where I wrote “Thinking about it,”), this would be a contradiction… unless uniqueness analysis is broken and needs to be fixed: It must take into account that delegate have contexts, but it currently doesn’t. If a pure factory function returns a delegate type, the result is only unique if the delegate type has an immutable context (or no context at all), otherwise mutable indirections might exist in the context, which means the result is not (guaranteed to be) transitively unique, and thus shouldn’t convert to `immutable`. So, for your DIP, you should address the fix to uniqueness as well.Outside qualifiers don’t matter for `pure` factory delegates at all. Only `R delegate(…) pure immutable` provides enough guarantees to make a unique object, and it can always be invoked, no matter what outside qualifiers you apply to it. The next weaker qualification is `pure const inout shared` and it’s not enough: `inout` makes no guarantees (it could be *mutable*), and `const` plus `shared` isn’t enough as mutable indirections might exist.The issue is not using the delegate itself as the pure factory function. The issue is that a mutable delegate with a mutable context could be *returned* from a pure factory function and then converted to an immutable delegate with an "immutable" context. Check [the issue](https://github.com/dlang/dmd/issues/19131#issuecomment-2541990651) and you'll understand.
Feb 04
On Wednesday, 4 February 2026 at 17:04:41 UTC, Quirin Schroll wrote:On Friday, 16 January 2026 at 19:45:28 UTC, Dukc wrote:The spec does not describe how it works and it was difficult for me to figure it out so I thought it'd be better to explain it. Anyway, we're on same page in this regard.Here, I was describing how the language *currently* works. I agree the invariant you write about should hold at all times but currently, it does not.I’m not so much concerned how the language currently works because it’s broken. I trust you that you’re perfectly able to describe that for the DIP.I guess this is exactly what causes the hole I had just discovered in my proposal when writing this.I was just writing that I'd be more liberal I don't see an issue converting `R delegate() const` to `R delegate immutable`, which is also allowed right now, because the context pointer isn't immutable so you don't end up actually breaking the assumption of the function you're pointing to.You do break the assumption because the member function attribute `immutable` on a delegate means “the context cannot change” and not “the context will not be changed by a call to this”; the implication is that a `int* delegate() immutable pure` returns unique objects that can be converted to `immutable`, but a `int* delegate() const pure` doesn’t provide enough guarantees for that.I see - a function pointer that is encoded as a delegate. Anyway, I still don't feel it's in scope for this proposal.Do you mean that we could write `int function(int) safe` alternatively as `int delegate(int) safe function`? I think this is also off scope for the DIP I'm thinking.My idea is that in `int delegate(int) safe function`, the `function` encodes that the context is guaranteed to be unused (and `null`).I'm going to propose making delegate qualifiers transitive, meaning you can have `R delegate() immutable` and `immutable(R delegate() immutable)`, but not what is currently `immutable(R delegate())` - my proposal will make that equal to `immutable(R delegate() immutable)`. Same with other type qualifiers. Since that disallows some types that you can specify currently, I was questioning whether those types are worth it. I feel the simplicity of transitivity outweighs the slightly higher type system power that non-transitive delegate qualifiers have.Would the fact your `immutable(R delegate() const shared)` needs to be `const shared(R delegate())` instead be such a major issue that the complications are worth it?I’m not sure I understand you correctly. An `immutable(R delegate() const shared)` fall into the same category as the `immutable(R delegate())`: If I’m right, it can only originate from a `R delegate() immutable`, and thus it’s fine, more or less coincidentally.So, for your DIP, you should address the fix to uniqueness as well.This is exactly what I was trying to do when I wrote that the pure factory function rule must not allow adding `immutable` or `const` to a delegate, and I guess `inout` needs to be disallowed too. With this I mean both the function pointer qualifier and the outer qualifier.
Feb 09
On Monday, 9 February 2026 at 19:19:37 UTC, Dukc wrote:On Wednesday, 4 February 2026 at 17:04:41 UTC, Quirin Schroll wrote:If I understand you correctly, for every qualifier `q`, you want to make `q(R delegate())` to be another way to spell `q(R delegate() q)` (exactly like the `const(shared int)` is the same as `shared(const int)`. That work for `immutable` if/because all properly formed `immutable(R delegate())` objects are either null or derived from a `immutable(R delegate() immutable)` or something that could be legally have that type. I’d still advise against that and only make `immutable(R delegate())` convert implicitly to `immutable(R delegate() immutable)`. The other way around is already covered.On Friday, 16 January 2026 at 19:45:28 UTC, Dukc wrote:I'm going to propose making delegate qualifiers transitive, meaning you can have `R delegate() immutable` and `immutable(R delegate() immutable)`, but not what is currently `immutable(R delegate())` - my proposal will make that equal to `immutable(R delegate() immutable)`.Would the fact your `immutable(R delegate() const shared)` needs to be `const shared(R delegate())` instead be such a major issue that the complications are worth it?I’m not sure I understand you correctly. An `immutable(R delegate() const shared)` fall into the same category as the `immutable(R delegate())`: If I’m right, it can only originate from a `R delegate() immutable`, and thus it’s fine, more or less coincidentally.Same with other type qualifiers.That doesn’t work. It can’t work for `const` specifically because - as a delegate member function attribute, `const` means calls won’t change the context, and - you can bind a variable of type `T` to a `const(T)` reference. The `const` reference might be useless (e.g. because the type only has mutable operations), but you should always be able to bind the reference! Illustrative example: ```d void main() { int* p; void delegate() pure dg = () { p = (p ? null : new int); }; const ref cdg = dg; // Proposed: typeof(cdg) is const(bool delegate() const pure) cdg(); } ``` If the type of `cdg` is `const(bool delegate() pure)` (which is what I suggest), it cannot be called. Annoying, but it has to be like this because calling it breaks the type system. If you insist on making `const(bool delegate() pure)` an alternative spelling for `const(bool delegate() const pure)`, you must allow the call, so have to reject the reference binding `cdg = dg`. You simply cannot end up allowing the call to `cdg` because that leads to a contradiction: Calling `cdg` has a side effect but is specified not to have one: Since `cdg` only has access to `const` data, the call is strongly pure (i.e. free of side effects) and calls to strongly pure `void` functions can be elided because they can’t legally do anything. The situation isn’t much different if `dg` (and `cdg` likewise) returns a value: ```d void main() { int* p; int* delegate() pure dg = () => p = (p ? null : new int); const ref cdg = dg; // Proposed: typeof(cdg) is const(bool delegate() const pure) bool a = cdg() is null; bool b = cdg() is null; } ``` Since `cdg` is annotated `const` and `pure`, calls with no operations that could change the context *must* return the same result, thus the second call can be changed to `bool b = a`. (This is called [common subexpression elimination](https://en.wikipedia.org/wiki/Common_subexpression_elimination) in optimizer circles.) Even worse, an `int* delegate() const pure` is guaranteed to return unique results: The resulting mutable `int*` can only be `null` or come from a newly allocated value; after all, it cannot originate from an argument (there are none) and it cannot originate from the context because the context is `const` and you can’t obtain an `int*` from `const` data. Since `cdg` doesn’t return a unique result, the type system must ban calling it.Since that disallows some types that you can specify currently, I was questioning whether those types are worth it. I feel the simplicity of transitivity outweighs the slightly higher type system power that non-transitive delegate qualifiers have.It can only be done consistently for `immutable`. While the context of a `const(int delegate())` and a `int delegate() const` are both `const`, they are `const` for different reasons. Both `const` mean: The invocation cannot change the context. The member function attribute says that the function pointer won’t change the context. The lack thereof means no such guarantee exists. For that reason, a `const(int delegate() const)` can be invoked and a `const(int delegate())`. You can’t treat an object that lacks an ability as something that has it.The whole point of pure factory functions is that the result can be converted to `immutable` or `const shared` implicitly.So, for your DIP, you should address the fix to uniqueness as well.This is exactly what I was trying to do when I wrote that the pure factory function rule must not allow adding `immutable` or `const` to a delegate, and I guess `inout` needs to be disallowed too. With this I mean both the function pointer qualifier and the outer qualifier.
Feb 17
On Tuesday, 17 February 2026 at 14:30:08 UTC, Quirin Schroll wrote:If the type of `cdg` is `const(bool delegate() pure)` (which is what I suggest), it cannot be called. Annoying, but it has to be like this because calling it breaks the type system.OK. Also without `pure`. So presumably a top-level attribute on a delegate `d` would only apply to `d.funcptr`, not `d.ptr`. Currently we have: ```d const int delegate() cd; pragma(msg, typeof(cd)); // const pragma(msg, typeof(cd.funcptr)); // not const pragma(msg, typeof(cd.ptr)); // not const ``` Gives: ``` const(int delegate()) int function() void* ```If you insist on making `const(bool delegate() pure)` an alternative spelling for `const(bool delegate() const pure)`, you must allow the call, so have to reject the reference binding `cdg = dg`.Even that is not sufficient. Steve:So basically, any struct or class instance cannot convert to const (or via pure constructor to immutable) if it contains a non-const delegate.Timon:for class references, you never know whether some subclass might have a mutable delegate fieldhttps://github.com/dlang/dmd/issues/18504#issuecomment-2541966629 (for easier reading https://bugzilla-archive.dlang.org/bugs/9149/). ```d class A { abstract void f() const; } class B : A { void delegate() d; override void f() const => d(); } void main() { B b = new B; int i; b.d = { i++; }; A a = b; const A ca = a; ca.f; // `i` mutated } ```
Feb 18
The more I think about this, the more I suspect that the language isn't doing enough to explain and align with slots. Timon touches upon it for this case:The fix I originally proposed in the first post removes theunsoundness inthe const system, but it is ugly. It means the line "const x = y;"fails ify is a delegate, but not if y is an instance of a struct wrapping a delegate. A more elegant solution would be to disallow calling const delegates whose context is not typed const or immutable. I.e. const(int delegate()const) can be called, but const(int delegate()) cannot becalled. In essence the function pointer and context pointer should not be getting type qualifiers applied to it. When you dereference the container (class in this case), it'll then get applied to the context pointer only. But we don't have syntax for this.
Feb 18
On Wednesday, 18 February 2026 at 13:18:17 UTC, Nick Treleaven wrote:Steve:Ouch, that's bad! Maybe it also means that `void[]` can't be assigned to `const(void)[]` as the array might contain a delegate that mutates it's context. OTOH you wouldn't be able to call it without unsafely casting the type to something else but there might be some loophole in that. Anyway, this is a reason for me to reconsider whether the more complex non-transitive qualifier system might be worth it after all. I'll check the issue and post whether I've changed my mind later. Feel free to offer arguments either way in the meantime.So basically, any struct or class instance cannot convert to const (or via pure constructor to immutable) if it contains a non-const delegate.Timon:for class references, you never know whether some subclass might have a mutable delegate field
Feb 18
On Wednesday, 18 February 2026 at 17:50:56 UTC, Dukc wrote:On Wednesday, 18 February 2026 at 13:18:17 UTC, Nick Treleaven wrote:I have decided that the non-transitive system (as I define it here) is needed. Otherwise, not only one couldn't `const`-qualify a virtual object, a virtual member function with a `const` context pointer wouldn't compile. Imposing such a limitation is out of question. Thanks for convincing me while I'm still writing, as it would have been annoying to change the title of the DIP after submitting it!Steve:Anyway, this is a reason for me to reconsider whether the more complex non-transitive qualifier system might be worth it after all. I'll check the issue and post whether I've changed my mind later. Feel free to offer arguments either way in the meantime.So basically, any struct or class instance cannot convert to const (or via pure constructor to immutable) if it contains a non-const delegate.Timon:for class references, you never know whether some subclass might have a mutable delegate field
Feb 23
On Wednesday, 18 February 2026 at 13:18:17 UTC, Nick Treleaven wrote:On Tuesday, 17 February 2026 at 14:30:08 UTC, Quirin Schroll wrote:Yes, it’s not relevant and represents arbitrary attributes.If the type of `cdg` is `const(bool delegate() pure)` (which is what I suggest), it cannot be called. Annoying, but it has to be like this because calling it breaks the type system.OK. Also without `pure`.So presumably a top-level attribute on a delegate `d` would only apply to `d.funcptr`, not `d.ptr`. Currently we have: ```d const int delegate() cd; pragma(msg, typeof(cd)); // const pragma(msg, typeof(cd.funcptr)); // not const pragma(msg, typeof(cd.ptr)); // not const ``` Gives: ``` const(int delegate()) int function() void* ```Obviously, a delegate type must pass qualifiers to its constituent parts: `typeof(cd.ptr)` must be `const(void*)` and `typeof(cd.funcptr)` must be `const(int function())`.Good, actually, because I want `const` references to be able to bind things.If you insist on making `const(bool delegate() pure)` an alternative spelling for `const(bool delegate() const pure)`, you must allow the call, so have to reject the reference binding `cdg = dg`.Even that is not sufficient.Steve:I guess the only way to avoid this is if * `const(void delegate())` is a type that’s not equivalent to `const(void delegate() const)`, and * an object of type `const(void delegate())` cannot be invoked.So basically, any struct or class instance cannot convert to `const` (or via pure constructor to `immutable`) if it contains a non-`const` delegate.Timon:for class references, you never know whether some subclass might have a mutable delegate fieldhttps://github.com/dlang/dmd/issues/18504#issuecomment-2541966629 (for easier reading https://bugzilla-archive.dlang.org/bugs/9149/). ```d class A { abstract void f() const; } class B : A { void delegate() d; override void f() const => d(); } void main() { B b = new B; int i; b.d = { i++; }; A a = b; const A ca = a; ca.f; // `i` mutated } ```
Feb 19
On Tuesday, 17 February 2026 at 14:30:08 UTC, Quirin Schroll wrote:If I understand you correctly, for every qualifier `q`, you want to make `q(R delegate())` to be another way to spell `q(R delegate() q)` (exactly like the `const(shared int)` is the same as `shared(const int)`.Assuming you meant `const(shared int)` is the same as `const(const shared int)`. But anyway, you understood my aim right.If you insist on making `const(bool delegate() pure)` an alternative spelling for `const(bool delegate() const pure)`, you must allow the call, so have to reject the reference binding `cdg = dg`.That's my intention. You won't be able to assign anything to `const(bool delegate())` that you can't assign to `bool delegate() const`. What I'm interested to hear, why would you prefer non-transitive delegate qualifiers? You couldn't call the delegates anyway that the transitive system disallows.The whole point of pure factory functions is that the result can be converted to `immutable` or `const shared` implicitly.I guess this means that strongly pure functions that return delegates with a mutable context won't be factory functions anymore. Too bad, but again: if we keep the non-transitive qualifier system so that such a factory function is allowed, we'll have to disallow calling any `immutable` or `const` delegates produced by it anyway. To me, uncallable delegates sound pretty... useless.
Feb 18
On Wednesday, 18 February 2026 at 17:40:31 UTC, Dukc wrote:On Tuesday, 17 February 2026 at 14:30:08 UTC, Quirin Schroll wrote:Yeah, kind of. I was after “There are many fundamentally equivalent ways to say the same type, e.g. qualifiers simply commute” and you’re saying the same but with “duplicate qualifiers don’t change anything.” We agree on this, AFAICT.If I understand you correctly, for every qualifier `q`, you want to make `q(R delegate())` to be another way to spell `q(R delegate() q)` (exactly like the `const(shared int)` is the same as `shared(const int)`.Assuming you meant `const(shared int)` is the same as `const(const shared int)`. But anyway, you understood my aim right.This isn’t a copy-assignment, it’s the binding of a reference. This might seem nitpicky, but I purposefully chose a reference because that is a fundamental operation that cannot be hooked/disabled by the programmer. You can user-define/disable copy-assignment, but you cannot, ever, change how binding a reference works or disable it. The example isn’t binding an otherwise unqualified reference, i.e. `ref _ = dg`, which always works (even with what you’re proposing). The example is binding a `const` reference: ```d const ref cdg = dg; ``` My sense is that should always work because absolutely all types should reference-convert into their `const` version, i.e. `is(T : const(T))` should be true for absolutely all `T`. You’re essentially proposing that `int delegate()` has no `const` version. You might say that by that standard, `const(int[])` isn’t really the `const` version of `int[]` either, after all, if you add and remove `const`, you end up with `const(int)[]`, and that’s true. The difference is that `int[]` is convertible to `const(int)[]`, which is why `is(int[] : const int[])` is true, whereas `int delegate()` isn’t convertible to `int delegate() const` (in fact, it’s the other way around). On `const(int)[]`, the qualifier is a restriction of what you can do with it; on `int delegate() const`, the qualifier is a guarantee the object makes to you. You can always restrict yourself, but you can’t pretend something makes a guarantee when it doesn’t. There are two options, as far as I see: 1. Delegates break the assumption that you can bind `const` references for any and all types. 2. Some delegate objects cannot invoked. IMO, the second option is way more natural. A `const(R delegate())` object then is much more like a `const` struct or class type with only mutable member functions: Those cannot be invoked. Nothing special. Also, common type analysis would break: ```d alias M = int delegate(); alias C = const(int delegate() const); ``` What’s the common type for `M` and `C`? Let’s go through the steps: 1. `M` converts to `const(M)` (adding `const` is always possible) 2. `C` converts to `const(int delegate())` (forgetting a guarantee is always possible) Thus, `M` and `C` have the common type `const(int delegate())`. If you make it so `const(int delegate())` basically doesn’t exist, `M` and `C` don’t have a common type, which makes very little sense. You also seem to miss that non-callable delegates are not entirely useless. They can be compared to `null` or compared for identity with `is`. That means a generic algorithm that takes its argument by `in` can’t work with delegates. An example would be this: ```d size_t indexOf(T)(in T[] values, in T value) { foreach (i, ref v; values) if (v is value) return i; return size_t.max; } ```If you insist on making `const(bool delegate() pure)` an alternative spelling for `const(bool delegate() const pure)`, you must allow the call, so have to reject the reference binding `cdg = dg`.That's my intention. You won't be able to assign anything to `const(bool delegate())` that you can't assign to `bool delegate() const`.What I'm interested to hear, why would you prefer non-transitive delegate qualifiers? You couldn't call the delegates anyway that the transitive system disallows.The qualifier would be transitive. You’re over-interpreting what that means, though. The outside qualifier still protects the context from mutation, but the lack of the member function attribute `const` means the internal function pointer does not guarantee that the context won’t be written to. You cannot add this guarantee from the outside. Transitivity of `const` means that `const(int[])` is the same as `const(const(int)[])`. It doesn’t mean that `const(void function(ref int))` is the same as `const(void function(const ref int))`. (A function pointer guaranteeing that it doesn’t alter a parameter is very similar to a delegate guaranteeing it won’t change its context.) As a member function attribute, `const` is a guarantee the function makes.That makes very little sense. Think about it like a class object. The result is unique. The context has been newly allocated. It should convert to `immutable`. If the class type doesn’t support any operations on `immutable` objects (usually through `const` member functions), doing the conversion to `immutable` is counter-productive, but perfectly valid. I’d say delegates fall in the same category. If the member function attributes aren’t sufficient (I think `const shared` is needed), calling the delegate shouldn’t be allowed.The whole point of pure factory functions is that the result can be converted to `immutable` or `const shared` implicitly.I guess this means that strongly pure functions that return delegates with a mutable context won't be factory functions anymore.Too bad, but again: if we keep the non-transitive qualifier system so that such a factory function is allowed, we'll have to disallow calling any `immutable` or `const` delegates produced by it anyway. To me, uncallable delegates sound pretty... useless.They are, but avoiding them will cause a thousand problems in other areas. Allowing them to exist and be useless is actually better.
Feb 19
On Friday, 5 December 2025 at 16:37:35 UTC, Dukc wrote:On the other hand, `imm = &s1.constFun;` is dangerous. While `constFun` itself wont mutate the struct, immutability assumes no-one will mutate the data. The compiler is free to assume the context of the delegate will remain untouched by other things done in the calling function, yet clearly the assumption could be easily broken by mutating `s1`, being a mutable struct. Also, an immutable delegate could be stored to thread-shared memory, and the results of calling it would depend on a thread-local context - the very issue `immutable` and `shared` are supposed to prevent.I was mistaken on this point. In my example, `imm` is typed as the function taking an immutable context pointer, but the delegate itself is not immutable and therefore the context pointer is neither. Therefore the issues I write about don't apply and this is safe.
Dec 29 2025
On Friday, 5 December 2025 at 16:37:35 UTC, Dukc wrote:You might be wondering why `mut = &s2.immutableFun;` should be allowed in safe code - a context pointer to mutable even when the struct is immutable. Well, the pointer is typed as `void*`. You can't mutate through that in ` safe` code, and calling the delegate will not do so either since the referred function actually takes the context pointer as immutable. The compiler guarantees it does, as it rejects delegates like `&s2.mutableFun`.I still think this point stands, but changing it isn't necessary to fix the type system hole. My actual focus is in qualifier transitivity and disallowing the willy-nilly top-level conversions between them.
Dec 29 2025
Started to write the actual DIP now. Doesn't mean the discussion needs to stop though. The DIP will probably need revisions anyway so the earlier the feedback, the better.
Feb 16
On 2/16/26 19:47, Dukc wrote:Started to write the actual DIP now. Doesn't mean the discussion needs to stop though. The DIP will probably need revisions anyway so the earlier the feedback, the better.Sorry, I mean to respond to this initially, but was too busy at the time and then forgot about this thread. Here is what I think is the principled way to address this. The type `T delegate(S)q` is an _existential type_: ``` ∃C. q(C)*×(T function(S,q(C)*)) ``` (`×` is cartesian product, so this is an existential type of pairs. Overall this type is saying: there exists some type `C` such that the delegate is a pair of context pointer of type `q(C)*` and a function that additionally to the argument types `S` accepts a context pointer of type `q(C)*` before returning a result of type `T`.) When we invoke a `T delegate(S)q`, we additionally pass the context pointer. It follows that we can invoke a `T delegate(S)q` only if for all types `C`, we can pass an argument of type `q(C)*` to a parameter of type `q(C)*`. What does this mean for subtyping of delegates `T delegate(S)q₁` and `T delegate(S)q₂`? We write `T ⊆ S` for "T is a subtype of S". a) we can remove any qualifier, for example: ``` T delegate(S)q const = ∃C. q(const(C))*×(T function(S,q(const(C))*)) ⊆ ∃C'.∃C. q(C')*×(T function(S,q(C')*)) | C'=const(C) = ∃C'. q(C')*×(T function(S,q(C')*)) = ∃C. q(C)*×(T function(S,q(C)*)) = T delegate(S)q ``` b) similarly, instead of removing them outright, we can widen context qualifiers: ``` T delegate(S)immutable = ∃C. immutable(C)*×(T function(S,immutable(C)*)) = ∃C. const(immutable(C))*×(T function(S,const(immutable(C))*)) ⊆ ∃C'.∃C. const(C')*×(T function(S,const(C')*)) | C'=immutable(C) = ∃C'. const(C')*×(T function(S,const(C')*)) = ∃C. const(C)*×(T function(S,const(C)*)) = T delegate(S)const ``` What happens when we qualify it? ``` q₁(T delegate(S)q₂) = q₁(∃C. q₂(C)*×(T function(S,q₂(C)*))) = ∃C. q₁(q₂(C)*×(T function(S,q₂(C)*))) = ∃C. q₁(q₂(C)*)×q₁(T function(S,q₂(C)*)) So the context is typed `q₁(q₂(C)*)`, but the function accepts a `q₂(C)*`. It follows that we can only invoke the delegate if for all types `C`, we can pass an argument of type `q₁(q₂(C)*)` to a parameter of type `q₂(C)*`. I think it suffices to consider `C=int*` as all rules applying to `int*` in this context would apply universally. (If a `q₁`-qualified function cannot be invoked, the call is also illegal.) For example, we cannot call `const(T delegate(S))`, but `immutable(T delegate(S)const)` can be called. Finally, let's look into subtyping in full generality: ``` q₁(T delegate(S)q₂) ⊆ q₃(T delegate(S)q₄) ⇔ (∃C. q₁(q₂(C)*)×q₁(T function(S,q₂(C)*)q₂)) ⊆ ∃C'. q₃(q₄(C'))×q₃(T function(S,q₄(C')*)) ⇔ ∀C.∃C'. q₁(q₂(C)*)×q₁(T function(S,q₂(C)*)) ⊆ q₃(q₄(C')*)×q₃(T function(S,q₄(C')*)) ⇔ ∀C.∃C'. q₁(q₂(C)*) ⊆ q₃(q₄(C')*) ∧ q₁(immutable(void)*)⊆q₃(immutable(void)*) ∧ q₄(C')* ⊆ q₂(C)* ⇔ q₁(immutable(void)*)⊆q₃(immutable(void)*)∧∀C.∃C'. q₁(q₂(C)*) ⊆ q₃(q₄(C')*) ∧ q₄(C')* ⊆ q₂(C)* ⇔ q₁(immutable(void)*)⊆q₃(immutable(void)*)∧∀C.∃q'. q₁(q₂(C)*) ⊆ q₃(q₄(q'(C))*) ∧ q₄(q'(C))* ⊆ q₂(C)* ⇔ q₁(immutable(void)*)⊆q₃(immutable(void)*)∧∃q'. q₁(q₂(int*)*) ⊆ q₃(q₄(q'(int*))*) ∧ q₄(q'(int*))* ⊆ q₂(int*)* ``` We can probably simplify this further, but this is already easily computable. The following metaprogram uses my derivations above to compute whether specific delegates should be callable as well as the implicit conversion relation for delegate qualifiers. I am ignoring the `inout` rabbit hole here, that would need to be worked out similarly (which is a bit annoying because the outer `inout` qualifier around the function type is not the same as the inner `inout` context qualifier, where the latter would need to be consistent with any `inout`s on parameter types instead). I think if the DIP does not agree with the facts computed by the metaprogram, it is probably broken. Note that implicit conversions are not the same as subtypes, and for implicit conversions we do not actually care about qualified function pointer convertibility, but I have left in the constraint on function pointer convertibility so that it can be easily modified to compute related relations. ```d import std; alias applyQuals(string[] quals,T) = mixin(quals.map!(x=>text(x,`(`)).join~`T`~')'.repeat(quals.length).text); alias Ptr(T)=T*; alias id(alias a)=a; enum allQuals = [[],["immutable"],["const"],["shared"],["const","shared"]]; // TODO: `inout`, `const inout`, `shared const inout` enum callable(string[] q1,string[] q2)=is(applyQuals!(q1,Ptr!(applyQuals!(q2,int*))):Ptr!(applyQuals!(q2,int*))); template converts(string[] q1,string[] q2,string[] q3,string[] q4){ alias converts=id!(false); static if(is(applyQuals!(q1,immutable(void)*):applyQuals!(q3,immutable(void)*))){ static foreach(qp;allQuals){ converts = id!( converts|| is(applyQuals!(q1,Ptr!(applyQuals!(q2,int*))):applyQuals!(q3,Ptr!(applyQ als!(q4~qp,int*)))) && is(Ptr!(applyQuals!(q4~qp,int*)):Ptr!(applyQuals!(q2,int*))) ); } } } auto dgstr(string[] q1, string[] q2)=>q1.map!(x=>text(x,`(`)).join~`T delegate(S)`~q2.join(" ")~')'.repeat(q1.length).text; static foreach(q1,q2;cartesianProduct(allQuals,allQuals)){ static if(callable!(q1,q2)){ pragma(msg, "callable: ",dgstr(q1,q2)); }else{ pragma(msg, "not callable: ",dgstr(q1,q2)); } } static foreach(q1,q2,q3,q4;cartesianProduct(allQuals,allQuals,allQuals,allQuals)){ static if(converts!(q1,q2,q3,q4)){ pragma(msg, `is(`,dgstr(q1,q2),` : `,dgstr(q3,q4),`)`); }else{ pragma(msg, `!is(`,dgstr(q1,q2),` : `,dgstr(q3,q4),`)`); } } ``` Output: callable: T delegate(S) callable: T delegate(S)immutable callable: T delegate(S)const callable: T delegate(S)shared callable: T delegate(S)const shared not callable: immutable(T delegate(S)) callable: immutable(T delegate(S)immutable) callable: immutable(T delegate(S)const) not callable: immutable(T delegate(S)shared) callable: immutable(T delegate(S)const shared) not callable: const(T delegate(S)) callable: const(T delegate(S)immutable) callable: const(T delegate(S)const) not callable: const(T delegate(S)shared) callable: const(T delegate(S)const shared) not callable: shared(T delegate(S)) callable: shared(T delegate(S)immutable) not callable: shared(T delegate(S)const) callable: shared(T delegate(S)shared) callable: shared(T delegate(S)const shared) not callable: const(shared(T delegate(S))) callable: const(shared(T delegate(S)immutable)) not callable: const(shared(T delegate(S)const)) not callable: const(shared(T delegate(S)shared)) callable: const(shared(T delegate(S)const shared)) is(T delegate(S) : T delegate(S)) !is(T delegate(S) : T delegate(S)immutable) !is(T delegate(S) : T delegate(S)const) !is(T delegate(S) : T delegate(S)shared) !is(T delegate(S) : T delegate(S)const shared) !is(T delegate(S) : immutable(T delegate(S))) !is(T delegate(S) : immutable(T delegate(S)immutable)) !is(T delegate(S) : immutable(T delegate(S)const)) !is(T delegate(S) : immutable(T delegate(S)shared)) !is(T delegate(S) : immutable(T delegate(S)const shared)) is(T delegate(S) : const(T delegate(S))) !is(T delegate(S) : const(T delegate(S)immutable)) !is(T delegate(S) : const(T delegate(S)const)) !is(T delegate(S) : const(T delegate(S)shared)) !is(T delegate(S) : const(T delegate(S)const shared)) !is(T delegate(S) : shared(T delegate(S))) !is(T delegate(S) : shared(T delegate(S)immutable)) !is(T delegate(S) : shared(T delegate(S)const)) !is(T delegate(S) : shared(T delegate(S)shared)) !is(T delegate(S) : shared(T delegate(S)const shared)) !is(T delegate(S) : const(shared(T delegate(S)))) !is(T delegate(S) : const(shared(T delegate(S)immutable))) !is(T delegate(S) : const(shared(T delegate(S)const))) !is(T delegate(S) : const(shared(T delegate(S)shared))) !is(T delegate(S) : const(shared(T delegate(S)const shared))) is(T delegate(S)immutable : T delegate(S)) is(T delegate(S)immutable : T delegate(S)immutable) is(T delegate(S)immutable : T delegate(S)const) is(T delegate(S)immutable : T delegate(S)shared) is(T delegate(S)immutable : T delegate(S)const shared) is(T delegate(S)immutable : immutable(T delegate(S))) is(T delegate(S)immutable : immutable(T delegate(S)immutable)) is(T delegate(S)immutable : immutable(T delegate(S)const)) is(T delegate(S)immutable : immutable(T delegate(S)shared)) is(T delegate(S)immutable : immutable(T delegate(S)const shared)) is(T delegate(S)immutable : const(T delegate(S))) is(T delegate(S)immutable : const(T delegate(S)immutable)) is(T delegate(S)immutable : const(T delegate(S)const)) is(T delegate(S)immutable : const(T delegate(S)shared)) is(T delegate(S)immutable : const(T delegate(S)const shared)) is(T delegate(S)immutable : shared(T delegate(S))) is(T delegate(S)immutable : shared(T delegate(S)immutable)) is(T delegate(S)immutable : shared(T delegate(S)const)) is(T delegate(S)immutable : shared(T delegate(S)shared)) is(T delegate(S)immutable : shared(T delegate(S)const shared)) is(T delegate(S)immutable : const(shared(T delegate(S)))) is(T delegate(S)immutable : const(shared(T delegate(S)immutable))) is(T delegate(S)immutable : const(shared(T delegate(S)const))) is(T delegate(S)immutable : const(shared(T delegate(S)shared))) is(T delegate(S)immutable : const(shared(T delegate(S)const shared))) is(T delegate(S)const : T delegate(S)) !is(T delegate(S)const : T delegate(S)immutable) is(T delegate(S)const : T delegate(S)const) !is(T delegate(S)const : T delegate(S)shared) !is(T delegate(S)const : T delegate(S)const shared) !is(T delegate(S)const : immutable(T delegate(S))) !is(T delegate(S)const : immutable(T delegate(S)immutable)) !is(T delegate(S)const : immutable(T delegate(S)const)) !is(T delegate(S)const : immutable(T delegate(S)shared)) !is(T delegate(S)const : immutable(T delegate(S)const shared)) is(T delegate(S)const : const(T delegate(S))) !is(T delegate(S)const : const(T delegate(S)immutable)) is(T delegate(S)const : const(T delegate(S)const)) !is(T delegate(S)const : const(T delegate(S)shared)) !is(T delegate(S)const : const(T delegate(S)const shared)) !is(T delegate(S)const : shared(T delegate(S))) !is(T delegate(S)const : shared(T delegate(S)immutable)) !is(T delegate(S)const : shared(T delegate(S)const)) !is(T delegate(S)const : shared(T delegate(S)shared)) !is(T delegate(S)const : shared(T delegate(S)const shared)) !is(T delegate(S)const : const(shared(T delegate(S)))) !is(T delegate(S)const : const(shared(T delegate(S)immutable))) !is(T delegate(S)const : const(shared(T delegate(S)const))) !is(T delegate(S)const : const(shared(T delegate(S)shared))) !is(T delegate(S)const : const(shared(T delegate(S)const shared))) is(T delegate(S)shared : T delegate(S)) !is(T delegate(S)shared : T delegate(S)immutable) !is(T delegate(S)shared : T delegate(S)const) is(T delegate(S)shared : T delegate(S)shared) !is(T delegate(S)shared : T delegate(S)const shared) !is(T delegate(S)shared : immutable(T delegate(S))) !is(T delegate(S)shared : immutable(T delegate(S)immutable)) !is(T delegate(S)shared : immutable(T delegate(S)const)) !is(T delegate(S)shared : immutable(T delegate(S)shared)) !is(T delegate(S)shared : immutable(T delegate(S)const shared)) is(T delegate(S)shared : const(T delegate(S))) !is(T delegate(S)shared : const(T delegate(S)immutable)) !is(T delegate(S)shared : const(T delegate(S)const)) is(T delegate(S)shared : const(T delegate(S)shared)) !is(T delegate(S)shared : const(T delegate(S)const shared)) is(T delegate(S)shared : shared(T delegate(S))) !is(T delegate(S)shared : shared(T delegate(S)immutable)) !is(T delegate(S)shared : shared(T delegate(S)const)) is(T delegate(S)shared : shared(T delegate(S)shared)) !is(T delegate(S)shared : shared(T delegate(S)const shared)) is(T delegate(S)shared : const(shared(T delegate(S)))) !is(T delegate(S)shared : const(shared(T delegate(S)immutable))) !is(T delegate(S)shared : const(shared(T delegate(S)const))) is(T delegate(S)shared : const(shared(T delegate(S)shared))) !is(T delegate(S)shared : const(shared(T delegate(S)const shared))) is(T delegate(S)const shared : T delegate(S)) !is(T delegate(S)const shared : T delegate(S)immutable) is(T delegate(S)const shared : T delegate(S)const) is(T delegate(S)const shared : T delegate(S)shared) is(T delegate(S)const shared : T delegate(S)const shared) !is(T delegate(S)const shared : immutable(T delegate(S))) !is(T delegate(S)const shared : immutable(T delegate(S)immutable)) !is(T delegate(S)const shared : immutable(T delegate(S)const)) !is(T delegate(S)const shared : immutable(T delegate(S)shared)) !is(T delegate(S)const shared : immutable(T delegate(S)const shared)) is(T delegate(S)const shared : const(T delegate(S))) !is(T delegate(S)const shared : const(T delegate(S)immutable)) is(T delegate(S)const shared : const(T delegate(S)const)) is(T delegate(S)const shared : const(T delegate(S)shared)) is(T delegate(S)const shared : const(T delegate(S)const shared)) is(T delegate(S)const shared : shared(T delegate(S))) !is(T delegate(S)const shared : shared(T delegate(S)immutable)) is(T delegate(S)const shared : shared(T delegate(S)const)) is(T delegate(S)const shared : shared(T delegate(S)shared)) is(T delegate(S)const shared : shared(T delegate(S)const shared)) is(T delegate(S)const shared : const(shared(T delegate(S)))) !is(T delegate(S)const shared : const(shared(T delegate(S)immutable))) is(T delegate(S)const shared : const(shared(T delegate(S)const))) is(T delegate(S)const shared : const(shared(T delegate(S)shared))) is(T delegate(S)const shared : const(shared(T delegate(S)const shared))) !is(immutable(T delegate(S)) : T delegate(S)) !is(immutable(T delegate(S)) : T delegate(S)immutable) !is(immutable(T delegate(S)) : T delegate(S)const) !is(immutable(T delegate(S)) : T delegate(S)shared) !is(immutable(T delegate(S)) : T delegate(S)const shared) is(immutable(T delegate(S)) : immutable(T delegate(S))) !is(immutable(T delegate(S)) : immutable(T delegate(S)immutable)) !is(immutable(T delegate(S)) : immutable(T delegate(S)const)) !is(immutable(T delegate(S)) : immutable(T delegate(S)shared)) !is(immutable(T delegate(S)) : immutable(T delegate(S)const shared)) is(immutable(T delegate(S)) : const(T delegate(S))) !is(immutable(T delegate(S)) : const(T delegate(S)immutable)) !is(immutable(T delegate(S)) : const(T delegate(S)const)) !is(immutable(T delegate(S)) : const(T delegate(S)shared)) !is(immutable(T delegate(S)) : const(T delegate(S)const shared)) !is(immutable(T delegate(S)) : shared(T delegate(S))) !is(immutable(T delegate(S)) : shared(T delegate(S)immutable)) !is(immutable(T delegate(S)) : shared(T delegate(S)const)) !is(immutable(T delegate(S)) : shared(T delegate(S)shared)) !is(immutable(T delegate(S)) : shared(T delegate(S)const shared)) is(immutable(T delegate(S)) : const(shared(T delegate(S)))) !is(immutable(T delegate(S)) : const(shared(T delegate(S)immutable))) !is(immutable(T delegate(S)) : const(shared(T delegate(S)const))) !is(immutable(T delegate(S)) : const(shared(T delegate(S)shared))) !is(immutable(T delegate(S)) : const(shared(T delegate(S)const shared))) is(immutable(T delegate(S)immutable) : T delegate(S)) is(immutable(T delegate(S)immutable) : T delegate(S)immutable) is(immutable(T delegate(S)immutable) : T delegate(S)const) is(immutable(T delegate(S)immutable) : T delegate(S)shared) is(immutable(T delegate(S)immutable) : T delegate(S)const shared) is(immutable(T delegate(S)immutable) : immutable(T delegate(S))) is(immutable(T delegate(S)immutable) : immutable(T delegate(S)immutable)) is(immutable(T delegate(S)immutable) : immutable(T delegate(S)const)) is(immutable(T delegate(S)immutable) : immutable(T delegate(S)shared)) is(immutable(T delegate(S)immutable) : immutable(T delegate(S)const shared)) is(immutable(T delegate(S)immutable) : const(T delegate(S))) is(immutable(T delegate(S)immutable) : const(T delegate(S)immutable)) is(immutable(T delegate(S)immutable) : const(T delegate(S)const)) is(immutable(T delegate(S)immutable) : const(T delegate(S)shared)) is(immutable(T delegate(S)immutable) : const(T delegate(S)const shared)) is(immutable(T delegate(S)immutable) : shared(T delegate(S))) is(immutable(T delegate(S)immutable) : shared(T delegate(S)immutable)) is(immutable(T delegate(S)immutable) : shared(T delegate(S)const)) is(immutable(T delegate(S)immutable) : shared(T delegate(S)shared)) is(immutable(T delegate(S)immutable) : shared(T delegate(S)const shared)) is(immutable(T delegate(S)immutable) : const(shared(T delegate(S)))) is(immutable(T delegate(S)immutable) : const(shared(T delegate(S)immutable))) is(immutable(T delegate(S)immutable) : const(shared(T delegate(S)const))) is(immutable(T delegate(S)immutable) : const(shared(T delegate(S)shared))) is(immutable(T delegate(S)immutable) : const(shared(T delegate(S)const shared))) is(immutable(T delegate(S)const) : T delegate(S)) is(immutable(T delegate(S)const) : T delegate(S)immutable) is(immutable(T delegate(S)const) : T delegate(S)const) is(immutable(T delegate(S)const) : T delegate(S)shared) is(immutable(T delegate(S)const) : T delegate(S)const shared) is(immutable(T delegate(S)const) : immutable(T delegate(S))) is(immutable(T delegate(S)const) : immutable(T delegate(S)immutable)) is(immutable(T delegate(S)const) : immutable(T delegate(S)const)) is(immutable(T delegate(S)const) : immutable(T delegate(S)shared)) is(immutable(T delegate(S)const) : immutable(T delegate(S)const shared)) is(immutable(T delegate(S)const) : const(T delegate(S))) is(immutable(T delegate(S)const) : const(T delegate(S)immutable)) is(immutable(T delegate(S)const) : const(T delegate(S)const)) is(immutable(T delegate(S)const) : const(T delegate(S)shared)) is(immutable(T delegate(S)const) : const(T delegate(S)const shared)) is(immutable(T delegate(S)const) : shared(T delegate(S))) is(immutable(T delegate(S)const) : shared(T delegate(S)immutable)) is(immutable(T delegate(S)const) : shared(T delegate(S)const)) is(immutable(T delegate(S)const) : shared(T delegate(S)shared)) is(immutable(T delegate(S)const) : shared(T delegate(S)const shared)) is(immutable(T delegate(S)const) : const(shared(T delegate(S)))) is(immutable(T delegate(S)const) : const(shared(T delegate(S)immutable))) is(immutable(T delegate(S)const) : const(shared(T delegate(S)const))) is(immutable(T delegate(S)const) : const(shared(T delegate(S)shared))) is(immutable(T delegate(S)const) : const(shared(T delegate(S)const shared))) !is(immutable(T delegate(S)shared) : T delegate(S)) !is(immutable(T delegate(S)shared) : T delegate(S)immutable) !is(immutable(T delegate(S)shared) : T delegate(S)const) !is(immutable(T delegate(S)shared) : T delegate(S)shared) !is(immutable(T delegate(S)shared) : T delegate(S)const shared) is(immutable(T delegate(S)shared) : immutable(T delegate(S))) !is(immutable(T delegate(S)shared) : immutable(T delegate(S)immutable)) !is(immutable(T delegate(S)shared) : immutable(T delegate(S)const)) is(immutable(T delegate(S)shared) : immutable(T delegate(S)shared)) !is(immutable(T delegate(S)shared) : immutable(T delegate(S)const shared)) is(immutable(T delegate(S)shared) : const(T delegate(S))) !is(immutable(T delegate(S)shared) : const(T delegate(S)immutable)) !is(immutable(T delegate(S)shared) : const(T delegate(S)const)) is(immutable(T delegate(S)shared) : const(T delegate(S)shared)) !is(immutable(T delegate(S)shared) : const(T delegate(S)const shared)) !is(immutable(T delegate(S)shared) : shared(T delegate(S))) !is(immutable(T delegate(S)shared) : shared(T delegate(S)immutable)) !is(immutable(T delegate(S)shared) : shared(T delegate(S)const)) !is(immutable(T delegate(S)shared) : shared(T delegate(S)shared)) !is(immutable(T delegate(S)shared) : shared(T delegate(S)const shared)) is(immutable(T delegate(S)shared) : const(shared(T delegate(S)))) !is(immutable(T delegate(S)shared) : const(shared(T delegate(S)immutable))) !is(immutable(T delegate(S)shared) : const(shared(T delegate(S)const))) is(immutable(T delegate(S)shared) : const(shared(T delegate(S)shared))) !is(immutable(T delegate(S)shared) : const(shared(T delegate(S)const shared))) is(immutable(T delegate(S)const shared) : T delegate(S)) is(immutable(T delegate(S)const shared) : T delegate(S)immutable) is(immutable(T delegate(S)const shared) : T delegate(S)const) is(immutable(T delegate(S)const shared) : T delegate(S)shared) is(immutable(T delegate(S)const shared) : T delegate(S)const shared) is(immutable(T delegate(S)const shared) : immutable(T delegate(S))) is(immutable(T delegate(S)const shared) : immutable(T delegate(S)immutable)) is(immutable(T delegate(S)const shared) : immutable(T delegate(S)const)) is(immutable(T delegate(S)const shared) : immutable(T delegate(S)shared)) is(immutable(T delegate(S)const shared) : immutable(T delegate(S)const shared)) is(immutable(T delegate(S)const shared) : const(T delegate(S))) is(immutable(T delegate(S)const shared) : const(T delegate(S)immutable)) is(immutable(T delegate(S)const shared) : const(T delegate(S)const)) is(immutable(T delegate(S)const shared) : const(T delegate(S)shared)) is(immutable(T delegate(S)const shared) : const(T delegate(S)const shared)) is(immutable(T delegate(S)const shared) : shared(T delegate(S))) is(immutable(T delegate(S)const shared) : shared(T delegate(S)immutable)) is(immutable(T delegate(S)const shared) : shared(T delegate(S)const)) is(immutable(T delegate(S)const shared) : shared(T delegate(S)shared)) is(immutable(T delegate(S)const shared) : shared(T delegate(S)const shared)) is(immutable(T delegate(S)const shared) : const(shared(T delegate(S)))) is(immutable(T delegate(S)const shared) : const(shared(T delegate(S)immutable))) is(immutable(T delegate(S)const shared) : const(shared(T delegate(S)const))) is(immutable(T delegate(S)const shared) : const(shared(T delegate(S)shared))) is(immutable(T delegate(S)const shared) : const(shared(T delegate(S)const shared))) !is(const(T delegate(S)) : T delegate(S)) !is(const(T delegate(S)) : T delegate(S)immutable) !is(const(T delegate(S)) : T delegate(S)const) !is(const(T delegate(S)) : T delegate(S)shared) !is(const(T delegate(S)) : T delegate(S)const shared) !is(const(T delegate(S)) : immutable(T delegate(S))) !is(const(T delegate(S)) : immutable(T delegate(S)immutable)) !is(const(T delegate(S)) : immutable(T delegate(S)const)) !is(const(T delegate(S)) : immutable(T delegate(S)shared)) !is(const(T delegate(S)) : immutable(T delegate(S)const shared)) is(const(T delegate(S)) : const(T delegate(S))) !is(const(T delegate(S)) : const(T delegate(S)immutable)) !is(const(T delegate(S)) : const(T delegate(S)const)) !is(const(T delegate(S)) : const(T delegate(S)shared)) !is(const(T delegate(S)) : const(T delegate(S)const shared)) !is(const(T delegate(S)) : shared(T delegate(S))) !is(const(T delegate(S)) : shared(T delegate(S)immutable)) !is(const(T delegate(S)) : shared(T delegate(S)const)) !is(const(T delegate(S)) : shared(T delegate(S)shared)) !is(const(T delegate(S)) : shared(T delegate(S)const shared)) !is(const(T delegate(S)) : const(shared(T delegate(S)))) !is(const(T delegate(S)) : const(shared(T delegate(S)immutable))) !is(const(T delegate(S)) : const(shared(T delegate(S)const))) !is(const(T delegate(S)) : const(shared(T delegate(S)shared))) !is(const(T delegate(S)) : const(shared(T delegate(S)const shared))) is(const(T delegate(S)immutable) : T delegate(S)) is(const(T delegate(S)immutable) : T delegate(S)immutable) is(const(T delegate(S)immutable) : T delegate(S)const) is(const(T delegate(S)immutable) : T delegate(S)shared) is(const(T delegate(S)immutable) : T delegate(S)const shared) is(const(T delegate(S)immutable) : immutable(T delegate(S))) is(const(T delegate(S)immutable) : immutable(T delegate(S)immutable)) is(const(T delegate(S)immutable) : immutable(T delegate(S)const)) is(const(T delegate(S)immutable) : immutable(T delegate(S)shared)) is(const(T delegate(S)immutable) : immutable(T delegate(S)const shared)) is(const(T delegate(S)immutable) : const(T delegate(S))) is(const(T delegate(S)immutable) : const(T delegate(S)immutable)) is(const(T delegate(S)immutable) : const(T delegate(S)const)) is(const(T delegate(S)immutable) : const(T delegate(S)shared)) is(const(T delegate(S)immutable) : const(T delegate(S)const shared)) is(const(T delegate(S)immutable) : shared(T delegate(S))) is(const(T delegate(S)immutable) : shared(T delegate(S)immutable)) is(const(T delegate(S)immutable) : shared(T delegate(S)const)) is(const(T delegate(S)immutable) : shared(T delegate(S)shared)) is(const(T delegate(S)immutable) : shared(T delegate(S)const shared)) is(const(T delegate(S)immutable) : const(shared(T delegate(S)))) is(const(T delegate(S)immutable) : const(shared(T delegate(S)immutable))) is(const(T delegate(S)immutable) : const(shared(T delegate(S)const))) is(const(T delegate(S)immutable) : const(shared(T delegate(S)shared))) is(const(T delegate(S)immutable) : const(shared(T delegate(S)const shared))) is(const(T delegate(S)const) : T delegate(S)) !is(const(T delegate(S)const) : T delegate(S)immutable) is(const(T delegate(S)const) : T delegate(S)const) !is(const(T delegate(S)const) : T delegate(S)shared) !is(const(T delegate(S)const) : T delegate(S)const shared) !is(const(T delegate(S)const) : immutable(T delegate(S))) !is(const(T delegate(S)const) : immutable(T delegate(S)immutable)) !is(const(T delegate(S)const) : immutable(T delegate(S)const)) !is(const(T delegate(S)const) : immutable(T delegate(S)shared)) !is(const(T delegate(S)const) : immutable(T delegate(S)const shared)) is(const(T delegate(S)const) : const(T delegate(S))) !is(const(T delegate(S)const) : const(T delegate(S)immutable)) is(const(T delegate(S)const) : const(T delegate(S)const)) !is(const(T delegate(S)const) : const(T delegate(S)shared)) !is(const(T delegate(S)const) : const(T delegate(S)const shared)) !is(const(T delegate(S)const) : shared(T delegate(S))) !is(const(T delegate(S)const) : shared(T delegate(S)immutable)) !is(const(T delegate(S)const) : shared(T delegate(S)const)) !is(const(T delegate(S)const) : shared(T delegate(S)shared)) !is(const(T delegate(S)const) : shared(T delegate(S)const shared)) !is(const(T delegate(S)const) : const(shared(T delegate(S)))) !is(const(T delegate(S)const) : const(shared(T delegate(S)immutable))) !is(const(T delegate(S)const) : const(shared(T delegate(S)const))) !is(const(T delegate(S)const) : const(shared(T delegate(S)shared))) !is(const(T delegate(S)const) : const(shared(T delegate(S)const shared))) !is(const(T delegate(S)shared) : T delegate(S)) !is(const(T delegate(S)shared) : T delegate(S)immutable) !is(const(T delegate(S)shared) : T delegate(S)const) !is(const(T delegate(S)shared) : T delegate(S)shared) !is(const(T delegate(S)shared) : T delegate(S)const shared) !is(const(T delegate(S)shared) : immutable(T delegate(S))) !is(const(T delegate(S)shared) : immutable(T delegate(S)immutable)) !is(const(T delegate(S)shared) : immutable(T delegate(S)const)) !is(const(T delegate(S)shared) : immutable(T delegate(S)shared)) !is(const(T delegate(S)shared) : immutable(T delegate(S)const shared)) is(const(T delegate(S)shared) : const(T delegate(S))) !is(const(T delegate(S)shared) : const(T delegate(S)immutable)) !is(const(T delegate(S)shared) : const(T delegate(S)const)) is(const(T delegate(S)shared) : const(T delegate(S)shared)) !is(const(T delegate(S)shared) : const(T delegate(S)const shared)) !is(const(T delegate(S)shared) : shared(T delegate(S))) !is(const(T delegate(S)shared) : shared(T delegate(S)immutable)) !is(const(T delegate(S)shared) : shared(T delegate(S)const)) !is(const(T delegate(S)shared) : shared(T delegate(S)shared)) !is(const(T delegate(S)shared) : shared(T delegate(S)const shared)) is(const(T delegate(S)shared) : const(shared(T delegate(S)))) !is(const(T delegate(S)shared) : const(shared(T delegate(S)immutable))) !is(const(T delegate(S)shared) : const(shared(T delegate(S)const))) is(const(T delegate(S)shared) : const(shared(T delegate(S)shared))) !is(const(T delegate(S)shared) : const(shared(T delegate(S)const shared))) is(const(T delegate(S)const shared) : T delegate(S)) !is(const(T delegate(S)const shared) : T delegate(S)immutable) is(const(T delegate(S)const shared) : T delegate(S)const) is(const(T delegate(S)const shared) : T delegate(S)shared) is(const(T delegate(S)const shared) : T delegate(S)const shared) !is(const(T delegate(S)const shared) : immutable(T delegate(S))) !is(const(T delegate(S)const shared) : immutable(T delegate(S)immutable)) !is(const(T delegate(S)const shared) : immutable(T delegate(S)const)) !is(const(T delegate(S)const shared) : immutable(T delegate(S)shared)) !is(const(T delegate(S)const shared) : immutable(T delegate(S)const shared)) is(const(T delegate(S)const shared) : const(T delegate(S))) !is(const(T delegate(S)const shared) : const(T delegate(S)immutable)) is(const(T delegate(S)const shared) : const(T delegate(S)const)) is(const(T delegate(S)const shared) : const(T delegate(S)shared)) is(const(T delegate(S)const shared) : const(T delegate(S)const shared)) is(const(T delegate(S)const shared) : shared(T delegate(S))) !is(const(T delegate(S)const shared) : shared(T delegate(S)immutable)) is(const(T delegate(S)const shared) : shared(T delegate(S)const)) is(const(T delegate(S)const shared) : shared(T delegate(S)shared)) is(const(T delegate(S)const shared) : shared(T delegate(S)const shared)) is(const(T delegate(S)const shared) : const(shared(T delegate(S)))) !is(const(T delegate(S)const shared) : const(shared(T delegate(S)immutable))) is(const(T delegate(S)const shared) : const(shared(T delegate(S)const))) is(const(T delegate(S)const shared) : const(shared(T delegate(S)shared))) is(const(T delegate(S)const shared) : const(shared(T delegate(S)const shared))) !is(shared(T delegate(S)) : T delegate(S)) !is(shared(T delegate(S)) : T delegate(S)immutable) !is(shared(T delegate(S)) : T delegate(S)const) !is(shared(T delegate(S)) : T delegate(S)shared) !is(shared(T delegate(S)) : T delegate(S)const shared) !is(shared(T delegate(S)) : immutable(T delegate(S))) !is(shared(T delegate(S)) : immutable(T delegate(S)immutable)) !is(shared(T delegate(S)) : immutable(T delegate(S)const)) !is(shared(T delegate(S)) : immutable(T delegate(S)shared)) !is(shared(T delegate(S)) : immutable(T delegate(S)const shared)) !is(shared(T delegate(S)) : const(T delegate(S))) !is(shared(T delegate(S)) : const(T delegate(S)immutable)) !is(shared(T delegate(S)) : const(T delegate(S)const)) !is(shared(T delegate(S)) : const(T delegate(S)shared)) !is(shared(T delegate(S)) : const(T delegate(S)const shared)) is(shared(T delegate(S)) : shared(T delegate(S))) !is(shared(T delegate(S)) : shared(T delegate(S)immutable)) !is(shared(T delegate(S)) : shared(T delegate(S)const)) !is(shared(T delegate(S)) : shared(T delegate(S)shared)) !is(shared(T delegate(S)) : shared(T delegate(S)const shared)) is(shared(T delegate(S)) : const(shared(T delegate(S)))) !is(shared(T delegate(S)) : const(shared(T delegate(S)immutable))) !is(shared(T delegate(S)) : const(shared(T delegate(S)const))) !is(shared(T delegate(S)) : const(shared(T delegate(S)shared))) !is(shared(T delegate(S)) : const(shared(T delegate(S)const shared))) is(shared(T delegate(S)immutable) : T delegate(S)) is(shared(T delegate(S)immutable) : T delegate(S)immutable) is(shared(T delegate(S)immutable) : T delegate(S)const) is(shared(T delegate(S)immutable) : T delegate(S)shared) is(shared(T delegate(S)immutable) : T delegate(S)const shared) is(shared(T delegate(S)immutable) : immutable(T delegate(S))) is(shared(T delegate(S)immutable) : immutable(T delegate(S)immutable)) is(shared(T delegate(S)immutable) : immutable(T delegate(S)const)) is(shared(T delegate(S)immutable) : immutable(T delegate(S)shared)) is(shared(T delegate(S)immutable) : immutable(T delegate(S)const shared)) is(shared(T delegate(S)immutable) : const(T delegate(S))) is(shared(T delegate(S)immutable) : const(T delegate(S)immutable)) is(shared(T delegate(S)immutable) : const(T delegate(S)const)) is(shared(T delegate(S)immutable) : const(T delegate(S)shared)) is(shared(T delegate(S)immutable) : const(T delegate(S)const shared)) is(shared(T delegate(S)immutable) : shared(T delegate(S))) is(shared(T delegate(S)immutable) : shared(T delegate(S)immutable)) is(shared(T delegate(S)immutable) : shared(T delegate(S)const)) is(shared(T delegate(S)immutable) : shared(T delegate(S)shared)) is(shared(T delegate(S)immutable) : shared(T delegate(S)const shared)) is(shared(T delegate(S)immutable) : const(shared(T delegate(S)))) is(shared(T delegate(S)immutable) : const(shared(T delegate(S)immutable))) is(shared(T delegate(S)immutable) : const(shared(T delegate(S)const))) is(shared(T delegate(S)immutable) : const(shared(T delegate(S)shared))) is(shared(T delegate(S)immutable) : const(shared(T delegate(S)const shared))) !is(shared(T delegate(S)const) : T delegate(S)) !is(shared(T delegate(S)const) : T delegate(S)immutable) !is(shared(T delegate(S)const) : T delegate(S)const) !is(shared(T delegate(S)const) : T delegate(S)shared) !is(shared(T delegate(S)const) : T delegate(S)const shared) !is(shared(T delegate(S)const) : immutable(T delegate(S))) !is(shared(T delegate(S)const) : immutable(T delegate(S)immutable)) !is(shared(T delegate(S)const) : immutable(T delegate(S)const)) !is(shared(T delegate(S)const) : immutable(T delegate(S)shared)) !is(shared(T delegate(S)const) : immutable(T delegate(S)const shared)) !is(shared(T delegate(S)const) : const(T delegate(S))) !is(shared(T delegate(S)const) : const(T delegate(S)immutable)) !is(shared(T delegate(S)const) : const(T delegate(S)const)) !is(shared(T delegate(S)const) : const(T delegate(S)shared)) !is(shared(T delegate(S)const) : const(T delegate(S)const shared)) is(shared(T delegate(S)const) : shared(T delegate(S))) !is(shared(T delegate(S)const) : shared(T delegate(S)immutable)) is(shared(T delegate(S)const) : shared(T delegate(S)const)) !is(shared(T delegate(S)const) : shared(T delegate(S)shared)) !is(shared(T delegate(S)const) : shared(T delegate(S)const shared)) is(shared(T delegate(S)const) : const(shared(T delegate(S)))) !is(shared(T delegate(S)const) : const(shared(T delegate(S)immutable))) is(shared(T delegate(S)const) : const(shared(T delegate(S)const))) !is(shared(T delegate(S)const) : const(shared(T delegate(S)shared))) !is(shared(T delegate(S)const) : const(shared(T delegate(S)const shared))) is(shared(T delegate(S)shared) : T delegate(S)) !is(shared(T delegate(S)shared) : T delegate(S)immutable) !is(shared(T delegate(S)shared) : T delegate(S)const) is(shared(T delegate(S)shared) : T delegate(S)shared) !is(shared(T delegate(S)shared) : T delegate(S)const shared) !is(shared(T delegate(S)shared) : immutable(T delegate(S))) !is(shared(T delegate(S)shared) : immutable(T delegate(S)immutable)) !is(shared(T delegate(S)shared) : immutable(T delegate(S)const)) !is(shared(T delegate(S)shared) : immutable(T delegate(S)shared)) !is(shared(T delegate(S)shared) : immutable(T delegate(S)const shared)) is(shared(T delegate(S)shared) : const(T delegate(S))) !is(shared(T delegate(S)shared) : const(T delegate(S)immutable)) !is(shared(T delegate(S)shared) : const(T delegate(S)const)) is(shared(T delegate(S)shared) : const(T delegate(S)shared)) !is(shared(T delegate(S)shared) : const(T delegate(S)const shared)) is(shared(T delegate(S)shared) : shared(T delegate(S))) !is(shared(T delegate(S)shared) : shared(T delegate(S)immutable)) !is(shared(T delegate(S)shared) : shared(T delegate(S)const)) is(shared(T delegate(S)shared) : shared(T delegate(S)shared)) !is(shared(T delegate(S)shared) : shared(T delegate(S)const shared)) is(shared(T delegate(S)shared) : const(shared(T delegate(S)))) !is(shared(T delegate(S)shared) : const(shared(T delegate(S)immutable))) !is(shared(T delegate(S)shared) : const(shared(T delegate(S)const))) is(shared(T delegate(S)shared) : const(shared(T delegate(S)shared))) !is(shared(T delegate(S)shared) : const(shared(T delegate(S)const shared))) is(shared(T delegate(S)const shared) : T delegate(S)) !is(shared(T delegate(S)const shared) : T delegate(S)immutable) is(shared(T delegate(S)const shared) : T delegate(S)const) is(shared(T delegate(S)const shared) : T delegate(S)shared) is(shared(T delegate(S)const shared) : T delegate(S)const shared) !is(shared(T delegate(S)const shared) : immutable(T delegate(S))) !is(shared(T delegate(S)const shared) : immutable(T delegate(S)immutable)) !is(shared(T delegate(S)const shared) : immutable(T delegate(S)const)) !is(shared(T delegate(S)const shared) : immutable(T delegate(S)shared)) !is(shared(T delegate(S)const shared) : immutable(T delegate(S)const shared)) is(shared(T delegate(S)const shared) : const(T delegate(S))) !is(shared(T delegate(S)const shared) : const(T delegate(S)immutable)) is(shared(T delegate(S)const shared) : const(T delegate(S)const)) is(shared(T delegate(S)const shared) : const(T delegate(S)shared)) is(shared(T delegate(S)const shared) : const(T delegate(S)const shared)) is(shared(T delegate(S)const shared) : shared(T delegate(S))) !is(shared(T delegate(S)const shared) : shared(T delegate(S)immutable)) is(shared(T delegate(S)const shared) : shared(T delegate(S)const)) is(shared(T delegate(S)const shared) : shared(T delegate(S)shared)) is(shared(T delegate(S)const shared) : shared(T delegate(S)const shared)) is(shared(T delegate(S)const shared) : const(shared(T delegate(S)))) !is(shared(T delegate(S)const shared) : const(shared(T delegate(S)immutable))) is(shared(T delegate(S)const shared) : const(shared(T delegate(S)const))) is(shared(T delegate(S)const shared) : const(shared(T delegate(S)shared))) is(shared(T delegate(S)const shared) : const(shared(T delegate(S)const shared))) !is(const(shared(T delegate(S))) : T delegate(S)) !is(const(shared(T delegate(S))) : T delegate(S)immutable) !is(const(shared(T delegate(S))) : T delegate(S)const) !is(const(shared(T delegate(S))) : T delegate(S)shared) !is(const(shared(T delegate(S))) : T delegate(S)const shared) !is(const(shared(T delegate(S))) : immutable(T delegate(S))) !is(const(shared(T delegate(S))) : immutable(T delegate(S)immutable)) !is(const(shared(T delegate(S))) : immutable(T delegate(S)const)) !is(const(shared(T delegate(S))) : immutable(T delegate(S)shared)) !is(const(shared(T delegate(S))) : immutable(T delegate(S)const shared)) !is(const(shared(T delegate(S))) : const(T delegate(S))) !is(const(shared(T delegate(S))) : const(T delegate(S)immutable)) !is(const(shared(T delegate(S))) : const(T delegate(S)const)) !is(const(shared(T delegate(S))) : const(T delegate(S)shared)) !is(const(shared(T delegate(S))) : const(T delegate(S)const shared)) !is(const(shared(T delegate(S))) : shared(T delegate(S))) !is(const(shared(T delegate(S))) : shared(T delegate(S)immutable)) !is(const(shared(T delegate(S))) : shared(T delegate(S)const)) !is(const(shared(T delegate(S))) : shared(T delegate(S)shared)) !is(const(shared(T delegate(S))) : shared(T delegate(S)const shared)) is(const(shared(T delegate(S))) : const(shared(T delegate(S)))) !is(const(shared(T delegate(S))) : const(shared(T delegate(S)immutable))) !is(const(shared(T delegate(S))) : const(shared(T delegate(S)const))) !is(const(shared(T delegate(S))) : const(shared(T delegate(S)shared))) !is(const(shared(T delegate(S))) : const(shared(T delegate(S)const shared))) is(const(shared(T delegate(S)immutable)) : T delegate(S)) is(const(shared(T delegate(S)immutable)) : T delegate(S)immutable) is(const(shared(T delegate(S)immutable)) : T delegate(S)const) is(const(shared(T delegate(S)immutable)) : T delegate(S)shared) is(const(shared(T delegate(S)immutable)) : T delegate(S)const shared) is(const(shared(T delegate(S)immutable)) : immutable(T delegate(S))) is(const(shared(T delegate(S)immutable)) : immutable(T delegate(S)immutable)) is(const(shared(T delegate(S)immutable)) : immutable(T delegate(S)const)) is(const(shared(T delegate(S)immutable)) : immutable(T delegate(S)shared)) is(const(shared(T delegate(S)immutable)) : immutable(T delegate(S)const shared)) is(const(shared(T delegate(S)immutable)) : const(T delegate(S))) is(const(shared(T delegate(S)immutable)) : const(T delegate(S)immutable)) is(const(shared(T delegate(S)immutable)) : const(T delegate(S)const)) is(const(shared(T delegate(S)immutable)) : const(T delegate(S)shared)) is(const(shared(T delegate(S)immutable)) : const(T delegate(S)const shared)) is(const(shared(T delegate(S)immutable)) : shared(T delegate(S))) is(const(shared(T delegate(S)immutable)) : shared(T delegate(S)immutable)) is(const(shared(T delegate(S)immutable)) : shared(T delegate(S)const)) is(const(shared(T delegate(S)immutable)) : shared(T delegate(S)shared)) is(const(shared(T delegate(S)immutable)) : shared(T delegate(S)const shared)) is(const(shared(T delegate(S)immutable)) : const(shared(T delegate(S)))) is(const(shared(T delegate(S)immutable)) : const(shared(T delegate(S)immutable))) is(const(shared(T delegate(S)immutable)) : const(shared(T delegate(S)const))) is(const(shared(T delegate(S)immutable)) : const(shared(T delegate(S)shared))) is(const(shared(T delegate(S)immutable)) : const(shared(T delegate(S)const shared))) !is(const(shared(T delegate(S)const)) : T delegate(S)) !is(const(shared(T delegate(S)const)) : T delegate(S)immutable) !is(const(shared(T delegate(S)const)) : T delegate(S)const) !is(const(shared(T delegate(S)const)) : T delegate(S)shared) !is(const(shared(T delegate(S)const)) : T delegate(S)const shared) !is(const(shared(T delegate(S)const)) : immutable(T delegate(S))) !is(const(shared(T delegate(S)const)) : immutable(T delegate(S)immutable)) !is(const(shared(T delegate(S)const)) : immutable(T delegate(S)const)) !is(const(shared(T delegate(S)const)) : immutable(T delegate(S)shared)) !is(const(shared(T delegate(S)const)) : immutable(T delegate(S)const shared)) !is(const(shared(T delegate(S)const)) : const(T delegate(S))) !is(const(shared(T delegate(S)const)) : const(T delegate(S)immutable)) !is(const(shared(T delegate(S)const)) : const(T delegate(S)const)) !is(const(shared(T delegate(S)const)) : const(T delegate(S)shared)) !is(const(shared(T delegate(S)const)) : const(T delegate(S)const shared)) is(const(shared(T delegate(S)const)) : shared(T delegate(S))) !is(const(shared(T delegate(S)const)) : shared(T delegate(S)immutable)) is(const(shared(T delegate(S)const)) : shared(T delegate(S)const)) !is(const(shared(T delegate(S)const)) : shared(T delegate(S)shared)) !is(const(shared(T delegate(S)const)) : shared(T delegate(S)const shared)) is(const(shared(T delegate(S)const)) : const(shared(T delegate(S)))) !is(const(shared(T delegate(S)const)) : const(shared(T delegate(S)immutable))) is(const(shared(T delegate(S)const)) : const(shared(T delegate(S)const))) !is(const(shared(T delegate(S)const)) : const(shared(T delegate(S)shared))) !is(const(shared(T delegate(S)const)) : const(shared(T delegate(S)const shared))) !is(const(shared(T delegate(S)shared)) : T delegate(S)) !is(const(shared(T delegate(S)shared)) : T delegate(S)immutable) !is(const(shared(T delegate(S)shared)) : T delegate(S)const) !is(const(shared(T delegate(S)shared)) : T delegate(S)shared) !is(const(shared(T delegate(S)shared)) : T delegate(S)const shared) !is(const(shared(T delegate(S)shared)) : immutable(T delegate(S))) !is(const(shared(T delegate(S)shared)) : immutable(T delegate(S)immutable)) !is(const(shared(T delegate(S)shared)) : immutable(T delegate(S)const)) !is(const(shared(T delegate(S)shared)) : immutable(T delegate(S)shared)) !is(const(shared(T delegate(S)shared)) : immutable(T delegate(S)const shared)) is(const(shared(T delegate(S)shared)) : const(T delegate(S))) !is(const(shared(T delegate(S)shared)) : const(T delegate(S)immutable)) !is(const(shared(T delegate(S)shared)) : const(T delegate(S)const)) is(const(shared(T delegate(S)shared)) : const(T delegate(S)shared)) !is(const(shared(T delegate(S)shared)) : const(T delegate(S)const shared)) !is(const(shared(T delegate(S)shared)) : shared(T delegate(S))) !is(const(shared(T delegate(S)shared)) : shared(T delegate(S)immutable)) !is(const(shared(T delegate(S)shared)) : shared(T delegate(S)const)) !is(const(shared(T delegate(S)shared)) : shared(T delegate(S)shared)) !is(const(shared(T delegate(S)shared)) : shared(T delegate(S)const shared)) is(const(shared(T delegate(S)shared)) : const(shared(T delegate(S)))) !is(const(shared(T delegate(S)shared)) : const(shared(T delegate(S)immutable))) !is(const(shared(T delegate(S)shared)) : const(shared(T delegate(S)const))) is(const(shared(T delegate(S)shared)) : const(shared(T delegate(S)shared))) !is(const(shared(T delegate(S)shared)) : const(shared(T delegate(S)const shared))) is(const(shared(T delegate(S)const shared)) : T delegate(S)) !is(const(shared(T delegate(S)const shared)) : T delegate(S)immutable) is(const(shared(T delegate(S)const shared)) : T delegate(S)const) is(const(shared(T delegate(S)const shared)) : T delegate(S)shared) is(const(shared(T delegate(S)const shared)) : T delegate(S)const shared) !is(const(shared(T delegate(S)const shared)) : immutable(T delegate(S))) !is(const(shared(T delegate(S)const shared)) : immutable(T delegate(S)immutable)) !is(const(shared(T delegate(S)const shared)) : immutable(T delegate(S)const)) !is(const(shared(T delegate(S)const shared)) : immutable(T delegate(S)shared)) !is(const(shared(T delegate(S)const shared)) : immutable(T delegate(S)const shared)) is(const(shared(T delegate(S)const shared)) : const(T delegate(S))) !is(const(shared(T delegate(S)const shared)) : const(T delegate(S)immutable)) is(const(shared(T delegate(S)const shared)) : const(T delegate(S)const)) is(const(shared(T delegate(S)const shared)) : const(T delegate(S)shared)) is(const(shared(T delegate(S)const shared)) : const(T delegate(S)const shared)) is(const(shared(T delegate(S)const shared)) : shared(T delegate(S))) !is(const(shared(T delegate(S)const shared)) : shared(T delegate(S)immutable)) is(const(shared(T delegate(S)const shared)) : shared(T delegate(S)const)) is(const(shared(T delegate(S)const shared)) : shared(T delegate(S)shared)) is(const(shared(T delegate(S)const shared)) : shared(T delegate(S)const shared)) is(const(shared(T delegate(S)const shared)) : const(shared(T delegate(S)))) !is(const(shared(T delegate(S)const shared)) : const(shared(T delegate(S)immutable))) is(const(shared(T delegate(S)const shared)) : const(shared(T delegate(S)const))) is(const(shared(T delegate(S)const shared)) : const(shared(T delegate(S)shared))) is(const(shared(T delegate(S)const shared)) : const(shared(T delegate(S)const shared)))
Feb 22
On 2/22/26 18:16, Timon Gehr wrote:callable: shared(T delegate(S)shared)(Note that this does not take into account other restrictions on `shared` such as nosharedaccess, that would prevent calling here. It is generally a good idea to not allow this call unless the language guarantees atomic access to the entire pair of context and function pointer. However, this is independent of the subtyping considerations.)
Feb 22
On Sunday, 22 February 2026 at 17:16:00 UTC, Timon Gehr wrote:The type `T delegate(S)q` is an _existential type_: ``` ∃C. q(C)*×(T function(S,q(C)*)) ```Haha, finally it pays off that I've dabbled with Haskell, so I can understand this without much difficulty despite having no CS degree.When we invoke a `T delegate(S)q`, we additionally pass the context pointer. It follows that we can invoke a `T delegate(S)q` only if for all types `C`, we can pass an argument of type `q(C)*` to a parameter of type `q(C)*`. What does this mean for subtyping of delegates `T delegate(S)q₁` and `T delegate(S)q₂`? We write `T ⊆ S` for "T is a subtype of S".I think you're saying the same thing as Quirin. Good to have a confirmation. Also thanks for generating the subtyping map.
Feb 23
DIP draft progress report: I have initial versions of all the sections written (unless I decide to add additional ones) but they still require revising. I thought I had the technical description pretty much done sans finishing code examples, but it turns out my treatment of rvalue/lvalue issues was badly flawed so I need to reconsider that part. Used vocabulary also needs thinking. I still wish to write the conversion rules in terms of co/contravariance as opposed to just what's implicitly convertible. I also have some terminology that I don't think is standartised yet (outer qualifier/context qualifier *set*) so maybe I need to add a glossary. Best case, I'll publish the draft 14th the week after the next.
Mar 18
On Wednesday, 18 March 2026 at 19:02:30 UTC, Dukc wrote:Best case, I'll publish the draft 14th the week after the next.Meant either: the 14th week, or the week after the next. I intended to edit the former form to the latter but did that only partially.
Mar 18
Thank you for taking on this tricky problem. Trying to figure it out will drive
one crazy. The path to figuring it out is to simplify the problem.
First off, let's get rid of the implicit "this" pointer and the struct, and
make
it explicit:
```
int fun(S* p);
```
and think about this in terms of function pointers:
```
int function(S* p) fp = &fun;
```
Then we just have to think about the qualifiers that go on the `S*`.
Fortunately, the rules for this is already resolved thru the notion of
"covariance". Covariance means we can add qualifiers, but not subtract them:
```
int function(const S*) fp = &fun; // Error
```
This must be an error because whoever calls `fp` will be expecting that its
parameter will not be modified, but `fun(p)` will be modifying it. On the other
hand:
```
int con(const S* p);
int function(S* p) cfp = &con;
```
works just fine, as `con()` will not modify p, as nothing there says it has to
modify it. We can say that `cfp` is covariant with `con`.
You'll see covariance in action in inheritance:
```
class C
{
int fun(int*);
int con(const int*);
}
class D : C
{
int fun(const int*); // OK, covariant
int con(int*); // Error, not covariant
}
```
If the delegate implicit conversion rules follow the covariant principle, then
it is correct. If it doesn't, it is broken and we must fix it.
BTW, contravariance applies to function return values and operates the opposite
way! Contravariance must also apply to implicit conversions of delegate return
types.
Mar 05
The spec does briefly cover covariance, but not contravariance: https://dlang.org/spec/function.html#covariance
Mar 05
An explanation of Covariance and Contravariance: https://medium.com/ andrew_johnson_4/understanding-covariance-and-contravariance-in-type-theory-fd9047f51db8 I want to emphasize that doing covariance and contravariance properly with delegates is not an option. Any deviation from the rules will lead to giant holes in D's type system and will make it unworkable. It would be like having a special rule that says "2+2=5". It won't work. The math is inevitable.
Mar 05
On Thursday, 5 March 2026 at 21:29:22 UTC, Walter Bright wrote:The spec does briefly cover covariance, but not contravariance: https://dlang.org/spec/function.html#covariancehttps://github.com/dlang/dlang.org/pull/4415
Mar 07
On Thursday, 5 March 2026 at 21:24:28 UTC, Walter Bright wrote:First off, let's get rid of the implicit "this" pointer and the struct, and make it explicit: ``` int fun(S* p); ``` and think about this in terms of function pointers: ``` int function(S* p) fp = &fun; ``` Then we just have to think about the qualifiers that go on the `S*`. Fortunately, the rules for this is already resolved thru the notion of "covariance". Covariance means we can add qualifiers, but not subtract them:This part is correct and already works as it should, as far as I know. No contest about the workings of plain function pointers.If the delegate implicit conversion rules follow the covariant principle, then it is correct. If it doesn't, it is broken and we must fix it.This part is where the problem is. Differently qualified delegates can and should be covariant and contravariant with each other, *but not in the same way as equivalent plain function pointers*. I suspect it's the failure to realise this that's behind the current issues. For demonstrating it, I'll borrow from my WIP DIP: ```D struct S { int field; safe pure int constFun() const => field; } safe main() { import std.stdio; S var; // Delegate mutable, this-reference assumed // to be immutable even though it isn't! int delegate() safe pure immutable del1 = &var.constFun; // Both the delegate and the this pointer // typed as immutable immutable int delegate() safe pure immutable del2 = del1; del2().writeln(); // mutating the "immutable" context of del2. var.field++; // del2 is immutable and pure so result of it's // call may or may not be cached. del2().writeln(); } ``` Also, we can currently convert between different top-level qualifications (as opposed to the context pointer qualifications) willy-nilly. Again, from the DIP: ```D safe pure fun(const int delegate() safe pure d) => d(); safe main() { import std.stdio; int i = 0; // Delegate is qualified as const but the context // pointer argument isn't which is why this // is allowed. const int delegate() safe pure callback = () => i++; callback.fun.writeln(); // The compiler may assume that callback, context // included, is unchanged since it is passed // as const. Since fun is pure, the compiler // is also allowed to cache the result of the // first call and elide the second call to callback, // assuming it'd return the same value as first time. // This optimisation, if done, will lead to // different value being printed. callback.fun.writeln(); } ``` These two are the problems we are talking about. I can (and probably will, since you asked) describe the proposed conversion rules with covariance and contravariance, but they are going to be different than they are right now.
Mar 06
On 3/6/2026 8:09 AM, Dukc wrote:These two are the problems we are talking about. I can (and probably will, since you asked) describe the proposed conversion rules with covariance and contravariance, but they are going to be different than they are right now.Yes, please describe the behavior in terms of covariance and contravariance. I also strongly recommend simplifying the examples. Lumping together safe pure and immutable into the same example is very confusing. Just start with const and not-const. One thing at a time, building on it. I went through the same process with DIP1000 and the co/contra for each of the qualifiers. What made DIP1000 hopelessly confusing was jumping into the deep end first. Think of a delegate as a pair, i.e. function pointer and this pointer. Any implicit conversions have to be co/contra for each, and it's much easier to understand that way.
Mar 06
On Friday, 6 March 2026 at 19:10:33 UTC, Walter Bright wrote:I also strongly recommend simplifying the examples. Lumping together safe pure and immutable into the same example is very confusing.`pure` is there to allow those optimisations (in the spec sense, without looking what the compilers presently do) that break the type system in those examples. ` safe` is needed to demonstrate that we indeed have a memory safety problem. If this only happened in ` system`/` trusted` code it'd be only an usability issue of a much lesser magnitude.Think of a delegate as a pair, i.e. function pointer and this pointer.I do, but with the caveats that: 1. The function will only be called with the context of the delegate, not any context even if similarily typed. 2. Neither the function pointer nor the context can be changed individually, except by the called function modifying the context. These invariants do allow some conversions that would be illegal were the delegate just a POD struct with freely mutable fields. For example, it is safe to remove any qualifier from the context pointer.
Mar 06
On Friday, 6 March 2026 at 21:33:01 UTC, Dukc wrote:` system` code can change the `.ptr` and `.funcptr` properties of a delegate, and as far as I can tell this is considered valid by the language spec (and therefore eligible for ` trusted`) as long as the new values have the correct types. So unfortunately I don't think you can actually rely on these assumptions.Think of a delegate as a pair, i.e. function pointer and this pointer.I do, but with the caveats that: 1. The function will only be called with the context of the delegate, not any context even if similarily typed. 2. Neither the function pointer nor the context can be changed individually, except by the called function modifying the context. These invariants do allow some conversions that would be illegal were the delegate just a POD struct with freely mutable fields. For example, it is safe to remove any qualifier from the context pointer.
Mar 06
On Saturday, 7 March 2026 at 02:04:52 UTC, Paul Backus wrote:On Friday, 6 March 2026 at 21:33:01 UTC, Dukc wrote:You're thinking something like ```D trusted fun(R delegate() del) { if(del.funcptr == &SomeKnownClass.memberFun) { (cast(SomeKnownClass) del.ptr).field = whatever; } return del; } ``` This function shouldn't necessarily be trusted, not with my proposal but not presently either. Reason is that `del` could be typed as `R delegate() const` at the client side. With the proposed changes it could also be qualified as `immutable`, `shared` or `inout`. Currently, this function can still be trusted if `SomeKnownClass.memberFun` requires a mutable context reference. There's no way in ` safe` code to obtain `R delegate() const` out of `R delegate()` and this is not changing. If `SomeKnownClass.memberFun` context reference would be `shared`, that would admittedly allow ` trusted` for `fun` presently but not with the proposed changes. However, why would you be doing that right now? You have to do an unsafe cast to obtain `R delegate()` out of `R delegate() shared`. Why not instead type `del` as `R delegate() shared`?` system` code can change the `.ptr` and `.funcptr` properties of a delegate, and as far as I can tell this is considered valid by the language spec (and therefore eligible for ` trusted`) as long as the new values have the correct types. So unfortunately I don't think you can actually rely on these assumptions.Think of a delegate as a pair, i.e. function pointer and this pointer.I do, but with the caveats that: 1. The function will only be called with the context of the delegate, not any context even if similarily typed. 2. Neither the function pointer nor the context can be changed individually, except by the called function modifying the context. These invariants do allow some conversions that would be illegal were the delegate just a POD struct with freely mutable fields. For example, it is safe to remove any qualifier from the context pointer.
Mar 07
On 3/7/26 10:59, Dukc wrote:You're thinking something like ```D trusted fun(R delegate() del) { if(del.funcptr == &SomeKnownClass.memberFun) { (cast(SomeKnownClass) del.ptr).field = whatever; } return del; } ``` This function shouldn't necessarily be trusted, not with my proposal but not presently either. Reason is that `del` could be typed as `R delegate() const` at the client side.Currently, the only valid way `del` can be typed as `R delegate()const` at the client side is if `SomeKnownClass.memberFun` is itself `const` and then the code is just invalid, DIP or no DIP.With the proposed changes it could also be qualified as `immutable`, `shared` or `inout`.You know which one is the ground truth because you identified the `funcptr`. What is true is that if the code above was valid before fixing the context qualifier handling, it is still valid after.
Mar 07
On 3/7/26 15:49, Timon Gehr wrote:What is true is that if the code above was valid"valid" -> "valid and not completely stupid"before fixing the context qualifier handling, it is still valid after.
Mar 07
On Saturday, 7 March 2026 at 09:59:43 UTC, Dukc wrote:On Saturday, 7 March 2026 at 02:04:52 UTC, Paul Backus wrote:No, I'm thinking of code that knows via out-of-band information what the type of the context is. For example: ```d import std.stdio; struct S { int n; void foo() { writeln("foo"); } void bar() { writeln("bar"); } } void main() { S s; void delegate() dg = &s.foo; dg.funcptr = &S.bar; dg(); // prints bar } ``` As far as I can tell, this is valid D code according to the language spec, and anything that makes it invalid would be a breaking change to the language.` system` code can change the `.ptr` and `.funcptr` properties of a delegate, and as far as I can tell this is considered valid by the language spec (and therefore eligible for ` trusted`) as long as the new values have the correct types. So unfortunately I don't think you can actually rely on these assumptions.You're thinking something like ```D trusted fun(R delegate() del) { if(del.funcptr == &SomeKnownClass.memberFun) { (cast(SomeKnownClass) del.ptr).field = whatever; } return del; } ```
Mar 07
On 3/7/26 17:13, Paul Backus wrote:On Saturday, 7 March 2026 at 09:59:43 UTC, Dukc wrote:While this DIP will have breaking changes, I don't expect it to propose to make the code above illegal. But the assignment to `funcptr` with the subsequent call is only legal because we do have the additional information about the `ptr`, it's not simply a pair with two independent components. Walter had been arguing that we should treat the two components independently for qualifier type checking, which makes no sense, and is not even how it works currently.On Saturday, 7 March 2026 at 02:04:52 UTC, Paul Backus wrote:No, I'm thinking of code that knows via out-of-band information what the type of the context is. For example: ```d import std.stdio; struct S { int n; void foo() { writeln("foo"); } void bar() { writeln("bar"); } } void main() { S s; void delegate() dg = &s.foo; dg.funcptr = &S.bar; dg(); // prints bar } ``` As far as I can tell, this is valid D code according to the language spec, and anything that makes it invalid would be a breaking change to the language.` system` code can change the `.ptr` and `.funcptr` properties of a delegate, and as far as I can tell this is considered valid by the language spec (and therefore eligible for ` trusted`) as long as the new values have the correct types. So unfortunately I don't think you can actually rely on these assumptions.You're thinking something like ```D trusted fun(R delegate() del) { if(del.funcptr == &SomeKnownClass.memberFun) { (cast(SomeKnownClass) del.ptr).field = whatever; } return del; } ```
Mar 07
On Saturday, 7 March 2026 at 16:13:48 UTC, Paul Backus wrote:On Saturday, 7 March 2026 at 09:59:43 UTC, Dukc wrote:Although I said the langauge would assume you don't do this, I actually meant ` safe` code only. You will still be able to do this in unsafe code, but it's your responsibility to check those delegates don't in fact point to qualified data. Same as now. The difference is that with the DIP you can remove the any of the qualifiers in safe code (the context pointer inner qualifier, but not always the outer qualifier: `shared` can be removed from `R delegate() shared` but not from `shared(R delegate())`). Therefore, if your trusted function that mutates the delegate in place receives a delegate `R delegate()`, it has to remain safe to call even with a safe `R delegate() immutable`. Without the DIP, it is enough to be safe with any safe `R delegate()` or `R delegate const`. If there are immutable-context delegates your function can't safely deal with, you have to prevent ` safe` code from creating those delegates, or your function must be ` system`.On Saturday, 7 March 2026 at 02:04:52 UTC, Paul Backus wrote:No, I'm thinking of code that knows via out-of-band information what the type of the context is. For example: ```d import std.stdio; struct S { int n; void foo() { writeln("foo"); } void bar() { writeln("bar"); } } void main() { S s; void delegate() dg = &s.foo; dg.funcptr = &S.bar; dg(); // prints bar } ``` As far as I can tell, this is valid D code according to the language spec, and anything that makes it invalid would be a breaking change to the language.` system` code can change the `.ptr` and `.funcptr` properties of a delegate, and as far as I can tell this is considered valid by the language spec (and therefore eligible for ` trusted`) as long as the new values have the correct types. So unfortunately I don't think you can actually rely on these assumptions.You're thinking something like ```D trusted fun(R delegate() del) { if(del.funcptr == &SomeKnownClass.memberFun) { (cast(SomeKnownClass) del.ptr).field = whatever; } return del; } ```
Mar 07
On 3/7/26 03:04, Paul Backus wrote:On Friday, 6 March 2026 at 21:33:01 UTC, Dukc wrote:This is completely backwards. Whatever the assumptions that can be relied upon are is what ` trusted` code also has to uphold in order to be valid.` system` code can change the `.ptr` and `.funcptr` properties of a delegate, and as far as I can tell this is considered valid by the language spec (and therefore eligible for ` trusted`) as long as the new values have the correct types. So unfortunately I don't think you can actually rely on these assumptions.Think of a delegate as a pair, i.e. function pointer and this pointer.I do, but with the caveats that: 1. The function will only be called with the context of the delegate, not any context even if similarily typed. 2. Neither the function pointer nor the context can be changed individually, except by the called function modifying the context. These invariants do allow some conversions that would be illegal were the delegate just a POD struct with freely mutable fields. For example, it is safe to remove any qualifier from the context pointer.
Mar 07
On Saturday, 7 March 2026 at 14:01:55 UTC, Timon Gehr wrote:On 3/7/26 03:04, Paul Backus wrote:From ["Safe Values"][1]:` system` code can change the `.ptr` and `.funcptr` properties of a delegate, and as far as I can tell this is considered valid by the language spec (and therefore eligible for ` trusted`) as long as the new values have the correct types. So unfortunately I don't think you can actually rely on these assumptions.This is completely backwards. Whatever the assumptions that can be relied upon are is what ` trusted` code also has to uphold in order to be valid.A `delegate` is safe when: 1. its `.funcptr` property is null or refers to a function that matches or is covariant with the delegate type, and 2. its `.ptr` property is null or refers to a memory object that is in a form expected by the function.According to the spec, these are the assumptions that ` safe` code is allowed to rely on, and that ` trusted` code must uphold. I cannot find anything in the spec that says the `.funcptr` and `.ptr` properties must not be changed independently. [1]: https://dlang.org/spec/function.html#safe-values
Mar 07
On 3/7/26 17:10, Paul Backus wrote:On Saturday, 7 March 2026 at 14:01:55 UTC, Timon Gehr wrote:The text you quoted literally is saying the `.ptr` property must refer to a memory object "in a form expected by the function". So this is exactly a place where the spec is saying it's not simply a pair with two independent components. The way the OP stated it was a bit too restrictive because it was stated in terms of overly precise provenance, which is probably not relevant even with the DIP, but it's literally true in ` safe` code and serves to explain why this is not simply a pair with two independent components. In any case, my point was that this is a DIP, the spec will change. The spec is currently broken. Citing the spec to say "you can't rely on this" makes no sense. You can only say things like "that seems too restrictive, the following kind of code should still be legal".On 3/7/26 03:04, Paul Backus wrote:From ["Safe Values"][1]:` system` code can change the `.ptr` and `.funcptr` properties of a delegate, and as far as I can tell this is considered valid by the language spec (and therefore eligible for ` trusted`) as long as the new values have the correct types. So unfortunately I don't think you can actually rely on these assumptions.This is completely backwards. Whatever the assumptions that can be relied upon are is what ` trusted` code also has to uphold in order to be valid.A `delegate` is safe when: 1. its `.funcptr` property is null or refers to a function that matches or is covariant with the delegate type, and 2. its `.ptr` property is null or refers to a memory object that is in a form expected by the function.According to the spec, these are the assumptions that ` safe` code is allowed to rely on, and that ` trusted` code must uphold. I cannot find anything in the spec that says the `.funcptr` and `.ptr` properties must not be changed independently. [1]: https://dlang.org/spec/function.html#safe-values
Mar 07
On 3/6/26 20:10, Walter Bright wrote:Think of a delegate as a pair, i.e. function pointer and this pointer. Any implicit conversions have to be co/contra for each, and it's much easier to understand that way.This is not true. Delegates are not simply a pair. The delegate type does not track the full type of the context. It behaves like an existential type. https://forum.dlang.org/post/10nfdkh$2h73$1 digitalmars.com
Mar 07









Dukc <ajieskola gmail.com> 