Looks like we still haven’t found the grail yet…
TL;DR: Our relaxed coercion rules for opt (#110 and following) may lead to “hard” deserialization errors later on.
I seems that the following can’t all hold:
- We are sound in the higher-order case (as per IDL-Soundness)
- We have no subtyping checks while deserialization (one of our Design goals).
- Type Erasure (another of the design goal)
- Record Extensibility, at least in the form implemented right now.
Counter-example
Here is the counter example:
service A1 : { bar : () -> () }
service B1 : { baz : () -> () }
service C : {
init : (service A1, service B1) -> (); // stores (a1, b1)
run : () -> () ; // runs a1.bar(b1.baz())
}
If we pass A1 and B1 to C.init, all is well. C.run works fine.
Now we upgrade A1 to A2 and B1 to B2, as follows:
type WantsBool = service : { foo : (bool) -> () }
type WantsInt = service : { foo : (int) -> () }
service wantsInt : WantsInt
service A2 : { bar : (opt WantsBool) -> () } // calls `arg.foo(true)` if `arg` is not `null`
service B2 : { baz : () -> (opt WantsInt) } // returns (opt wantsInt)
These upgrades are permitted by our :<, and actually desirable (i.e. restricting <: to avoid “obviously bogus” upgrades doesn’t help)
But now C.run will cause B2 to pass opt wantsInt to A2. This will succeed at deserialization (because “Function and services references coerce unconditionally”). Now A2 invokes wantsInt.foo(true), and the deserialization of (true) at expected type (bool) will fail in wantsInt.
Generic Soundness proof
In https://github.com/dfinity/candid/blob/master/spec/IDL-Soundness.md#proof-for-canonical-subtyping we have a generic soundness proof for “Canonical subtyping”, that says “A solution with canonical subtyping (transitive, contravariant) is sound.”. And our <: is that!
But it’s not “decompositional” any more. In particular,
If t1 <: t2 and s1 in t1 <: s2 in t2 then s1 <: s2.
(as required in the generic proof) doesn’t hold .
Concretely in our example above, we have opt WantsInt <: opt WantsBool (because all opt types related). But because the reference value is passed through, this also means WantsInt in opt WantsInt <: WantsInt <: opt WantsBool, but we don’t have WantsInt <: WantsBool.
So…
Dunno. No concrete suggestions. It’s sunday morning, and I just wanted to get this insight out of my head.
Looks like we still haven’t found the grail yet…
TL;DR: Our relaxed coercion rules for opt (#110 and following) may lead to “hard” deserialization errors later on.
I seems that the following can’t all hold:
Counter-example
Here is the counter example:
If we pass
A1andB1toC.init, all is well.C.runworks fine.Now we upgrade
A1toA2andB1toB2, as follows:These upgrades are permitted by our
:<, and actually desirable (i.e. restricting<:to avoid “obviously bogus” upgrades doesn’t help)But now
C.runwill cause B2 to passopt wantsInttoA2. This will succeed at deserialization (because “Function and services references coerce unconditionally”). Now A2 invokeswantsInt.foo(true), and the deserialization of(true)at expected type(bool)will fail inwantsInt.Generic Soundness proof
In https://github.com/dfinity/candid/blob/master/spec/IDL-Soundness.md#proof-for-canonical-subtyping we have a generic soundness proof for “Canonical subtyping”, that says “A solution with canonical subtyping (transitive, contravariant) is sound.”. And our
<:is that!But it’s not “decompositional” any more. In particular,
(as required in the generic proof) doesn’t hold .
Concretely in our example above, we have
opt WantsInt <: opt WantsBool(because allopttypes related). But because the reference value is passed through, this also meansWantsInt in opt WantsInt <: WantsInt <: opt WantsBool, but we don’t haveWantsInt <: WantsBool.So…
Dunno. No concrete suggestions. It’s sunday morning, and I just wanted to get this insight out of my head.