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 (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
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 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









Dukc <ajieskola gmail.com> 