Constness and delegates
Timon Gehr
timon.gehr at gmx.ch
Fri Jan 10 02:50:16 UTC 2020
On 10.01.20 00:11, Mafi wrote:
> Regarding the work and comments on a pull request about delegate
> constness (https://github.com/dlang/dmd/pull/10644) I think there is
> some things we need to discuss.
>
> I think the only sane way to analyze the soundness of delegates is to
> equate:
>
> R delegate(P) qualifier f;
>
> with:
>
> interface I { R f(P) qualifier; }
> I f;
> ...
No, this is not exactly the right way to think about this. A delegate is
not an interface, because it *includes* an _opaque_ context pointer. The
qualifier on a delegate qualifies _both_ the function and the context
pointer and the reason why qualifiers can be dropped anyway is that
`qualified(void*)` can implicitly convert to `void*` (even though DMD
does not know it yet) and the delegate maintains an invariant that says
that the function pointer and the context pointer are compatible.
> Therefore given some class:
>
> class C { R f(P) const; }
> C c;
>
> The delegate &c.f is of type 'R f(P) const'! The const qualifier does
> *not* apply to the this-Pointer of the delegate but to the contract of
> the invocation.
This conclusion is however correct.
> Therefore the qualifier should be handled in a
> contravariant manner (and not covariant) because it describes the
> implicit this-Parameter of the referenced method and not the stored
> this-Pointer. The const-ness of the this-Pointer is the one "outside"
> the delegate.
> ...
Yes, exactly.
> qualifer1( R delegate(P) qualifier2 ) f;
>
> Is always a well-formed type / variable. But f can only be called iff
> qualifier1 is implicitly convertible to qualifier2. This solves the
> soundness of delegates in const classes and structs:
>
> auto s = S(3);
>
> struct S {
> int x = 0;
> void delegate() f;
>
> this(int x) { this.x = x; this.f = &this.incX; }
> void incX() { x++; }
> void const_method() const { f(); } // HERE
> }
>
> The line marked HERE compiles currently but f (in this const context) is
> of type 'const void delegate()' and therefore cannot be invoked (because
> const does not convert to mutable). If you change the type of f to "void
> delegate() const" it can be invoked but "incX" cannot be assigned to it!
> Soundness recovered. Const references cannot change any (implicit) state
> and immutable objects cannot observably change at all.
> ...
Yes. Also see: https://issues.dlang.org/show_bug.cgi?id=9149#c11
(Where I reached the same conclusion.)
> So in general:
>
> struct S { void f() qualifier2; }
> qualifier1 S s;
> auto f = &s.f;
>
> Is of type qualifier1(void delegate() qualifier2). Note the additional
> qualifier1 around the type. It (and not qualifier2) makes sure we
> respect the constness of the instance.
> ...
No. The type should be `void delegate() qualifier1 qualifier2`, given
that `f` can actually be called on `s`. (And otherwise the expression
should not compile, there is no reason to allow constructing a delegate
that will never be able to be called.)
> So what about implicit conversions? Well as always T -> const(T) <-
> immutable(T). Additionally qualifer(R delegate(P) const) -> qualifier(R
> delegate(P)), that is, we drop the const! This is because we loose
> power, the delegate cannot be invoked in const contexts anymore. This
> makes simple 'R delegate(P)' the goto-type for callbacks, as is probably
> the case anyways in most D code. Of course the inverse cannot be
> allowed, otherwise we lose the soundness again.
> ...
Yes. And it's not only `const`. You can lose *all* qualifiers.
> Additionally qualifier(R delegate(P) const) -> qualifier(R delegate(P)
> immutable). This way immutable R delegate(P) immutable can be initialized
> from a delegate to const method on an immutable object.
> ...
No, this would break the type system. An `R delegate(immutable(P))pure
immutable` can be implicitly memoized, but the same is not true for `R
delegate(immutable(P))pure const`, so there is no such subtyping
relationship.
It is however true that `&c.f` should have an immutable delegate type if
`c` is immutable and `f` is a `const` method, consistent with what I
stated above.
> Inline delegates that want to be const (either explicitely or maybe
> implicitely(?)) have to treat every referenced stack variable as const,
> like going through a const this-Pointer, which is actually what happens
> anyways.
>
> I am not sure exactly how to treat inout. And I don't know in what state
> shared is in general. So what do you think? Does this sound reasonable?
> Please discuss.
Some test cases:
This should compile:
void main(){
immutable(void*) a;
void* b=a; // this is rejected incorrectly
// TODO: add other qualifier combinations
}
This should compile too:
void main(){
int delegate()const dgc;
int delegate() dgc2=dgc; // this is correctly accepted
int delegate()immutable dgi;
int delegate() dgi2=dgi; // this is rejected incorrectly
int delegate() dgi3=()=>dgi(); // ugly workaround
int delegate()shared dgs;
int delegate() dgs2=dgs; // this is rejected incorrectly
int delegate() dgs3=()=>dgs(); // ugly workaround
// TODO: add all other qualifier combinations
}
Exhaustive tests for checks on nested function contexts:
void fun(inout(int)*){
int* x;
const(int*) cx;
immutable(int*) ix;
shared(int*) sx;
shared(const(int*)) scx;
inout(int*) wx;
shared(inout(int*)) swx;
const(inout(int*)) cwx;
shared(const(inout(int*))) scwx;
void foo(){
int* x=x;
const(int)* cx=cx; // ok
immutable(int)* ix=ix; // ok
shared(int)* sx=sx; // ok
shared(const(int*)) scx=scx; // ok
inout(int)* wx=wx; // ok
shared(inout(int))* swx=swx; // ok
const(inout(int))* cwx=cwx; // ok
shared(const(inout(int)))* scwx=scwx; // ok
}
void fooc()const{
int* x=x; // currently ok, shouldn't compile
const(int)* x2=x; // ok
const(int)* cx=cx; // ok
immutable(int)* ix=ix; // ok
shared(int)* sx=sx; // currently ok, shouldn't compile
const(shared(int))* sx2=sx; // ok
shared(const(int*)) scx=scx; // ok
inout(int)* wx=wx; // currently ok, shouldn't compile
const(inout(int))* wx2=wx; // ok
shared(inout(int))* swx=swx; // currently ok, shouldn't compile
shared(const(inout(int)))* swx2=swx; // ok
const(inout(int))* cwx=cwx; // ok
shared(const(inout(int)))* scwx=scwx; // ok
}
void fooi()immutable{
//int* x=x; // error, correct
//const(int)* cx=cx; // error, correct
immutable(int)* ix=ix; // ok
//shared(int)* sx=sx; // error, correct
//shared(const(int*)) scx=scx; // error, correct
//inout(int)* wx=wx; // error, correct
//shared(inout(int))* swx=swx; // error, correct
//const(inout(int))* cwx=cwx; // error, correct
//shared(const(inout(int)))* scwx=scwx; // error, correct
}
void foos()shared{
//int* x=x; // error, correct
//const(int)* cx=cx; // error, correct
immutable(int)* ix=ix; // ok
shared(int)* sx=sx; // ok
shared(const(int*)) scx=scx; // ok
//inout(int)* wx=wx; // error, correct
//shared(inout(int))* swx=swx; // currently error, should work
//const(inout(int))* cwx=cwx; // error, correct
//shared(const(inout(int)))* scwx=scwx; // currently error,
should work
}
void foosc()shared const{
//int* x=x; // error, correct
//const(int)* cx=cx; // error, correct
immutable(int)* ix=ix; // ok
//shared(int)* sx=sx; // error, correct
//const(shared(int))* sx2=sx; // currently error, should work
shared(const(int*)) scx=scx; // ok
//inout(int)* wx=wx; // error, correct
//const(inout(int))* wx2=wx; // currently error, should work
//shared(inout(int))* swx=swx; // error, correct
//const(shared(inout(int)))* swx2=swx; // currently error,
should work
//const(inout(int))* cwx=cwx; // error, correct
//shared(const(inout(int)))* scwx=scwx; // currently error,
should work
}
void foow()inout{
int* x=x; // currently ok, shouldn't compile
immutable(int)* ix=ix; // ok
shared(int)* sx=sx; // currently ok, shouldn't compile
inout(int)* wx=wx; // ok
shared(inout(int))* swx=swx; // ok
const(inout(int))* cwx=cwx; // ok
shared(const(inout(int)))* scwx=scwx; // ok
}
void foosw()shared inout{
//int* x=x; // error, correct
immutable(int)* ix=ix; // ok
//shared(int)* sx=sx; // error, correct
//inout(int)* wx=wx; // error, correct
shared(inout(int))* swx=swx; // ok
//const(inout(int))* cwx=cwx; // error, correct
shared(const(inout(int)))* scwx=scwx; // ok
}
void fooscw()shared const inout{
//int* x=x; // error, correct
immutable(int)* ix=ix; // ok
//shared(int)* sx=sx; // error, correct
//inout(int)* wx=wx; // error, correct
//shared(inout(int))* swx=swx; // error, correct
//const(shared(inout(int)))* swx2=swx; // currently error,
should compile
//const(inout(int))* cwx=cwx; // error, correct
shared(const(inout(int)))* scwx=scwx; // ok
}
}
void fun(inout(int)*){
void bar(){}
void barc()const{}
void bari()immutable{}
void bars()shared{}
void barsc()shared const{}
void barw()inout{}
void barsw()shared inout{}
void barcw()const inout{}
void barscw()shared const inout{}
void foo(){
bar(); // ok
barc(); // ok
bari(); // ok
bars(); // ok
barsc(); // ok
barsw(); // ok
barcw(); // ok
barscw(); // ok
}
void fooc()const{
bar(); // currently ok, shouldn't compile
barc(); // ok
bari(); // ok
bars(); // currently ok, shouldn't compile
barsc(); // ok
barsw(); // currently ok, shouldn't compile
barcw(); // ok
barscw(); // ok
}
void fooi()immutable{
bar(); // currently ok, shouldn't compile
barc(); // currently ok, shouldn't compile
bari(); // ok
bars(); // currently ok, shouldn't compile
barsc(); // currently ok, shouldn't compile
barsw(); // currently ok, shouldn't compile
barcw(); // currently ok, shouldn't compile
barscw(); // currently ok, shouldn't compile
}
void foos()shared{
bar(); // currently ok, shouldn't compile
barc(); // currently ok, shouldn't compile
bari(); // ok
bars(); // ok
barsc(); // ok
barsw(); // ok
barcw(); // currently ok, shouldn't compile
barscw(); // ok
}
void foosc()shared const{
bar(); // currently ok, shouldn't compile
barc(); // currently ok, shouldn't compile
bari(); // ok
bars(); // currently ok, shouldn't compile
barsc(); // ok
barsw(); // currently ok, shouldn't compile
barcw(); // currently ok, shouldn't compile
barscw(); // ok
}
void foow()inout{
bar(); // currently ok, shouldn't compile
barc(); // currently ok, shouldn't compile
bari(); // ok
bars(); // currently ok, shouldn't compile
barsc(); // currently ok, shouldn't compile
barsw(); // ok
barcw(); // ok
barscw(); // ok
}
void foosw()shared inout{
bar(); // currently ok, shouldn't compile
barc(); // currently ok, shouldn't compile
bari(); // ok
bars(); // currently ok, shouldn't compile
barsc(); // currently ok, shouldn't compile
barsw(); // ok
barcw(); // currently ok, shouldn't compile
barscw(); // ok
}
void fooscw()shared const inout{
bar(); // currently ok, shouldn't compile
barc(); // currently ok, shouldn't compile
bari(); // ok
bars(); // currently ok, shouldn't compile
barsc(); // currently ok, shouldn't compile
barsw(); // currently ok, shouldn't compile
barcw(); // currently ok, shouldn't compile
barscw(); // ok
}
}
More information about the Digitalmars-d
mailing list