Proposal: Casting Rules

Note

Feature awaiting further design consideration.

This is a proposal outlining potential rules for implicit casts.

Cast Operations

A cast operation is performed from one value type to another value type. A value type:

  • Cannot be const.
  • Cannot be an interface.

Cast operations are implemented using cast methods:

class Example {
        static int cast(Example value) noexcept;
}

Satisfies

A : B is a satisfies check, where A and B are two types. A : B can only be true if A contains all the methods required by B.

However if B is not an interface then A and B must have the same type (ignoring template arguments), which prevents two different but API-equivalent types from being cast to one another.

Many casts will depend on the result of a satisfies check:

template <typename T>
class ref_t {
        template <typename S>
        static ref_t<S> cast(ref_t<T> value) noexcept
                require(T : S);
}

Hence to cast from T& to S& it is necessary to check that T : S.

Noop

The noop tag indicates that a method doesn’t require any run-time code. It would be used on cast methods to indicate that they are equivalent to omitting the call:

template <typename T>
class RefWrap(T& value) {
        template <typename S>
        static RefWrap<S> cast(RefWrap<T> value) noexcept noop = default;
}

The noop property can only be achieved using = default and has method-specific meaning (e.g. it is different for destructors).

Cast Sequence

  • Copy reference targets (T& -> T) until reference depths match.

  • If Source = A& and Dest = B&:
    • If A : B: success!
    • Otherwise, copy source and continue.
  • If Source = A and Dest = B&:
    • If A : B: success!
    • If B is an interface: failure!
    • Otherwise, Dest = B.stripConst.
  • If Source = A and Dest = B:
    • If A has user implicit to B: success!
    • If B is a variant and A is in the variant: success!
  • Failure!

This pseudocode gives some more detail:

bool implicitCast(Type source, Type dest, bool canBind) {
        if ((source.refDepth + canBind ? 1 : 0) < dest.refDepth) {
                // We can only bind zero or one times.
                return false;
        }

        // Keep removing references from source type until we reach
        // depth of destination type.
        while (source.refDepth > dest.refDepth) {
                if (!source.target.implicitCopyable) { return false; }
                source = source.target.implicitCopyType;
        }

        if (source.refDepth == dest.refDepth) {
                if (source.isRef) {
                        return implicitCastRefToRef(source, dest, canBind);
                } else {
                        return implicitCastValueToValue(source, dest);
                }
        } else {
                assert (source.refDepth + 1) == dest.refDepth;
                return implicitCastValueToRef(source, dest);
        }
}

bool implicitCastRefToRef(Type source, Type dest, bool canBind) {
        assert source.isRef && dest.isRef;
        assert source.refDepth == dest.refDepth;

        // Try a polymorphic reference cast.
        if (satisfies(source.target, dest.target)) {
                return true;
        }

        // Reference types aren't compatible so we can try to copy, cast
        // and then bind.
        if (!source.target.implicitCopyable || !canBind) {
                return false;
        }

        source = source.target.implicitCopyType;

        return implicitCastValueToRef(source, dest);
}

bool implicitCastValueToRef(Type source, Type dest) {
        assert dest.isRef;

        // Try to perform a bind operation; this will be needed if the
        // destination target type is an interface.
        if (implicitCastNoop(source, dest.target)) {
                return true;
        }

        // Try to cast the source type to the destination type without
        // the const tag; if this is successful we can then bind.
        return implicitCastValueToValue(source, dest.target.stripConst);
}

bool implicitCastValueToValue(Type source, Type dest) {
        assert !source.isConst && !dest.isConst && !source.isInterface;
        if (dest.isInterface) { return false; }

        if (implicitCastNoop(source, dest)) {
                return true;
        }

        // Either we perform a user implicitCast() call or we cast from
        // a variant type to its parent variant object.
        return source.hasUserImplicitCast(dest) ||
               dest.hasVariantType(source);
}

bool implicitCastNoop(Type source, Type dest) {
        assert !source.isConst && !dest.isConst && !source.isInterface;
        return satisfies(source, dest);
}

bool satisfies(Type source, Type dest) {
        if (dest.isAuto) {
                // References can't match auto.
                if (source.isRef) { return false; }

                // Everything else does match auto.
                return true;
        }

        if (dest.isInterface) {
                return methodSet(source) >= methodSet(dest);
        }

        if (source.isObject != dest.isObject) {
                // Cannot cast template var -> object type or
                // vice versa.
                return false;
        }

        if (source.isObject) {
                if (!dest.isObject) {
                        // Cannot cast object -> template var.
                        return false;
                }

                if (source.objectType != dest.objectType) {
                        // Cannot cast between different object
                        // types.
                        return false;
                }
        } else {
                assert source.isTemplate;

                if (source.templateVar != dest.templateVar) {
                        // Cannot cast between different template
                        // types.
                        return false;
                }
        }

        if (source.isRef) {
                assert dest.isRef;
                return methodSet(source.target) >= methodSet(dest.target);
        }

        return methodSet(source) >= methodSet(dest);
}

Noop Casts

Noop casts are the most basic kind of cast, performed without any code executed at run-time:

const<A> Type -> auto
    where Type != OtherType&

const<A> ObjectType<...> -> const<B> ObjectType<...>
    where const<A> ObjectType<...> : const<B> ObjectType<...>

const<A> T -> const<B> T
    where const<A> T : const<B> T

Variant Parent Casts

A type can be cast to a variant that contains it:

Type -> VariantType
    where Type in VariantType

Deref Reference Casts

A reference-to-reference type can be cast to remove the outer reference:

Type&& -> Type&

Polymorphic Reference Casts

Type& -> Interface&

Polymorphic Static Reference Casts

staticref<Type> -> staticref<Interface>

Implicit Copy Ref

T& -> T

Implicit Copy Const

const T -> T

Bind

T -> T&