Existential casting of borrowed values

Hi folks,

I'm a mentee participating in the 2026 Swift Mentorship program. I have interest in the ownership system but I'm fairly new to the compiler implementation. While I'm investigating Borrowed value with `as` casting, I found that as operator might not have been updated with the new ownership system, and I would love to work on it. My goal of this post is to discuss

  • what's the intended behavior when as is used with borrowed values?
  • how should I approach adding this to the language, if this was needed?

Expected Behavior?

Casting with as operator on borrowed values has very limited support. For instance, the following snippet looks reasonable, but will fail during compilation. I have seen people raised similar issues [1] [2] in the past, so it does seem to be helpful to have such feature in the language.

protocol Prot {
    borrowing func test()
}

struct Value: Prot {
    borrowing func test() {
        // do something
    }
}

class Klass: Prot { 
    borrowing func test() {
        // do something
    }
}

func run(on v: borrowing Value) {
    // implicitly casting from struct to existential (v as any Prot)
    runProt(v)
}

func run(on k: borrowing Klass) {
    // implicitly casting from class to existential (k as any Prot)
    runProt(k)
}

func run<T: Prot>(v: borrowing T) {
    // implicitly casting from generic to existential (v as any Prot)
    runProt(v)
}

func runProt(_ v: borrowing any Prot) {
    v.test()
}

Error message:

❯ swift-frontend-dev -c existential.swift
existential.swift:17:13: error: 'v' is borrowed and cannot be consumed
15 | }
16 |
17 | func run(on v: borrowing Value) {
   |             `- error: 'v' is borrowed and cannot be consumed
18 |     // implicitly casting from struct to existential (v as any Prot)
19 |     runProt(v)
   |         `- note: consumed here
20 | }
21 |

existential.swift:22:13: error: 'k' is borrowed and cannot be consumed
20 | }
21 |
22 | func run(on k: borrowing Klass) {
   |             `- error: 'k' is borrowed and cannot be consumed
23 |     // implicitly casting from class to existential (k as any Prot)
24 |     runProt(k)
   |         `- note: consumed here
25 | }
26 |

existential.swift:27:19: error: 'v' is borrowed and cannot be consumed
25 | }
26 |
27 | func run<T: Prot>(v: borrowing T) {
   |                   `- error: 'v' is borrowed and cannot be consumed
28 |     // implicitly casting from generic to existential (v as any Prot)
29 |     runProt(v)
   |         `- note: consumed here
30 | }
31 |

The as operation can carry out many duties. Some of the casting cases seem to make sense to work with borrowed values:

  • existential casting:
    • from struct to existential
    • from class to existential
    • from generic to existential
    • anyP as any Q where P refines Q (already works)
  • class casting
    • from class to AnyObject
    • casting to the same type (a Klass instance casted to Klass: k as Klass, already works)
    • class upcast (already works)
  • tuple conversion: (Sub, Int) to (Super, Int)

Some of the casting cases don't seem to make sense:

  • String to NSString, Array to NSArray, NSString to CFString, etc.
  • optional inject (value as Value?)
  • optional widen (optionalSub as Optional<Super>)

It looks like similar cases for types conforming to ~Copyable have been considered, and yield different error messages. For instance, the following snippet

protocol Prot: ~Copyable {
    borrowing func foo() -> Int
}

struct Value: ~Copyable, Prot {
    borrowing func foo() -> Int {
        return 42
    }
}

func run(_ v: borrowing Value) {
    runProt(v)
    runOptional(v)
    v as any Prot
}

func runProt(_ p: borrowing any Prot) {
    p.foo()
}

func runOptional(_ p: borrowing Value?) {
    p?.foo()
}

will give error message:

❯ swift-frontend-dev -c typeof.swift
typeof.swift:12:13: error: argument type 'Value' does not conform to expected type 'Copyable'
10 | 
11 | func run(_ v: borrowing Value) {
12 |     runProt(v)
   |             `- error: argument type 'Value' does not conform to expected type 'Copyable'
13 |     runOptional(v)
14 |     v as any Prot

typeof.swift:13:17: error: implicit conversion to 'Value?' is consuming
11 | func run(_ v: borrowing Value) {
12 |     runProt(v)
13 |     runOptional(v)
   |                 |- error: implicit conversion to 'Value?' is consuming
   |                 `- note: add 'consume' to make consumption explicit
14 |     v as any Prot
15 | }

typeof.swift:14:5: error: cannot convert value of type 'Value' to type 'any Prot' in coercion
12 |     runProt(v)
13 |     runOptional(v)
14 |     v as any Prot
   |     `- error: cannot convert value of type 'Value' to type 'any Prot' in coercion
15 | }
16 | 

Potential Approach

New explicit syntax

An additional syntax can be introduced, as discussed in a previous pitch, to make borrow casting specific: borrow x as any P. This would allow us to surface more clear error messages, for example when users try to do borrow str as NSString.

New SIL support

For the first few examples of copyable types, the root cause seems to be that the SIL does a init_existential_addr, unwraps the @moveOnly wrapper of the borrowed value by owning the value, then does a store or copy_addr of the unwrapped value. It doesn't seem like the SIL has the capability to express placing a borrowed loadable value into a existential buffer without copying it.

For the existential casting cases, it seems we may need a new kind of SIL instruction that could work with borrowed address into existential.

We could lift the restriction of the existing store_borrow, so that it would work with init_existential_addr besides alloc_stack.

%1 = copyable_to_moveonlywrapper [owned] %0 : $S
%2 = move_value [lexical] %1 : $@moveOnly S
%3 = mark_unresolved_non_copyable_value [no_consume_or_assign] %2 : $@moveOnly S
%4  = alloc_stack $any P
%5  = init_existential_addr %4 : $*any P, $S
%6  = begin_borrow %3
%7  = moveonlywrapper_to_copyable [guaranteed] %6
%8  = store_borrow %7 to %5  // currently not possible 
%9  = open_existential_addr immutable_access %4 to $*@opened Self
%10 = witness_method $@opened Self, #P.f, %9
apply %10<@opened Self>(%9)
end_borrow %8
deinit_existential_addr %4
end_borrow %6
dealloc_stack %4

I'd appreciate guidance on a few things before I go further:

  • Is existential erasure of borrowed values unimplemented, or unresolved?
  • Is a new SIL instruction the right direction?
  • Would the new surface syntax (borrow x as any P, per the earlier pitch) be a reasonable candidate?

Thanks in advance!


  1. ↩︎

  2. ↩︎

9 Likes

I think this would make sense

1 Like

To clarify, you are referring to coercion or conversion with explicit as or implicit conversion, as opposed to casting via as?, right?

In order for these conversions to be doable as a borrow in full generality, we have to be able to use the value in its current representation at its current address. This rules out almost all of the operations you listed in the general case, except for the class upcasts. Existentials expect to carry their own copy of the value with separate ownership and wrapped inside of an existential box. Tuple conversions may shuffle the elements around, or may need to move them in memory to make room for elements that change size because of their conversions. By contrast, casting class references to another class type or to AnyObject does not change the value representation, so it can always be done in-place.

The exceptions for existential cases that I can see are:

  • Existential upcasts where the destination type's protocol constraints are a prefix of the source type's, as in any P & Q to any P, or any P to Any, since in these cases the destination representation is a prefix of the source representation, and the value could be used in-place without any memory operations.
  • Cases where the source value is a concrete type known to be "bitwise-borrowable", meaning that it is valid to bitwise-copy the value representation from one place to another via load_borrow then store_borrow, and then use the representation in the new location as a borrow of the original value. This is true of class references, and of structs, enums, or tuples that are composed only of BitwiseCopyable fields or class references.

"Bitwise-borrowable" is a concept that exists in the implementation and is tracked by the compiler and runtime, but it is not yet surfaced in the language. As such, although we could partially implement these conversions as borrowing in cases where the source type is known to be bitwise-borrowable, but we would also need to communicate to a developer why generic cases, or cases involving a concrete type that is not bitwise-borrowable, are not allowed.

1 Like