Let's preface the PR with: I'm not a compiler engineer. This fix was made with h…elp from Claude (max effort). While I've tested the effectiveness of the changes locally, I'm not sure how sound the changes actually are (I would think they are at least OK. If not actually good). You can feel free to close the PR and apply the fix yourself in a better way after reading the result of my investigations below.
Resolves https://github.com/swiftlang/swift/issues/89954.
##
### Proof of Issue
```bash
cat > issue-89954.swift <<'EOF'
@inline(never)
func asDecimal(_ x: UInt8, writeByte: (UInt8) -> Void) {
let (q, r) = x.quotientAndRemainder(dividingBy: 10)
writeByte(q &+ 48); writeByte(r &+ 48)
}
// A) Captures a MutableSpan (~Escapable): closure is wrapped in mark_dependence.
@inline(never)
public func viaMutableSpan(_ p: UnsafeMutableRawPointer, _ n: Int) -> Int {
var span = unsafe MutableSpan(_unsafeStart: p.assumingMemoryBound(to: UInt8.self), count: n)
var i = 0
asDecimal(123) { span[i] = $0; i &+= 1 }
asDecimal(45) { span[i] = $0; i &+= 1 }
return i
}
// B) Same shape, Copyable pointer (no mark_dependence): specializes today.
@inline(never)
public func viaBuffer(_ p: UnsafeMutableRawPointer, _ n: Int) -> Int {
let buf = unsafe UnsafeMutableBufferPointer(start: p.assumingMemoryBound(to: UInt8.self), count: n)
var i = 0
asDecimal(123) { buf[i] = $0; i &+= 1 }
asDecimal(45) { buf[i] = $0; i &+= 1 }
return i
}
EOF
swiftc -O -emit-sil issue-89954.swift | swift demangle
```
<details>
<summary>Click for the swift code but with syntax highlighting</summary>
```swift
@inline(never)
func asDecimal(_ x: UInt8, writeByte: (UInt8) -> Void) {
let (q, r) = x.quotientAndRemainder(dividingBy: 10)
writeByte(q &+ 48); writeByte(r &+ 48)
}
// A) Captures a MutableSpan (~Escapable): closure is wrapped in mark_dependence.
@inline(never)
public func viaMutableSpan(_ p: UnsafeMutableRawPointer, _ n: Int) -> Int {
var span = unsafe MutableSpan(_unsafeStart: p.assumingMemoryBound(to: UInt8.self), count: n)
var i = 0
asDecimal(123) { span[i] = $0; i &+= 1 }
asDecimal(45) { span[i] = $0; i &+= 1 }
return i
}
// B) Same shape, Copyable pointer (no mark_dependence): specializes today.
@inline(never)
public func viaBuffer(_ p: UnsafeMutableRawPointer, _ n: Int) -> Int {
let buf = unsafe UnsafeMutableBufferPointer(start: p.assumingMemoryBound(to: UInt8.self), count: n)
var i = 0
asDecimal(123) { buf[i] = $0; i &+= 1 }
asDecimal(45) { buf[i] = $0; i &+= 1 }
return i
}
```
</details>
<details>
<summary>Click for full result of the command on my machine</summary>
```bash
sil_stage canonical
import Builtin
import Swift
import SwiftShims
@inline(never) func asDecimal(_ x: UInt8, writeByte: (UInt8) -> ())
@inline(never) public func viaMutableSpan(_ p: UnsafeMutableRawPointer, _ n: Int) -> Int
@inline(never) public func viaBuffer(_ p: UnsafeMutableRawPointer, _ n: Int) -> Int
// main
// Isolation: unspecified
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
[%1: noescape **]
[global: ]
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
%2 = integer_literal $Builtin.Int32, 0 // user: %3
%3 = struct $Int32 (%2) // user: %4
return %3 // id: %4
} // end sil function 'main'
// asDecimal(_:writeByte:)
// Isolation: unspecified
sil hidden [noinline] @main.asDecimal(_: Swift.UInt8, writeByte: (Swift.UInt8) -> ()) -> () : $@convention(thin) (UInt8, @guaranteed @noescape @callee_guaranteed (UInt8) -> ()) -> () {
[%1: noescape **, read v**.c*.v**, write v**.c*.v**, copy v**.c*.v**, destroy v**.c*.v**]
[global: read,write,copy,destroy,allocate,deinit_barrier]
// %0 "x" // users: %5, %2
// %1 "writeByte" // users: %19, %15, %3
bb0(%0 : $UInt8, %1 : $@noescape @callee_guaranteed (UInt8) -> ()):
debug_value %0, let, name "x", argno 1 // id: %2
debug_value %1, let, name "writeByte", argno 2 // id: %3
%4 = integer_literal $Builtin.Int8, 10 // users: %7, %6
%5 = struct_extract %0, #UInt8._value // users: %7, %6
%6 = builtin "udiv_Int8"(%5, %4) : $Builtin.Int8 // users: %9, %12
%7 = builtin "urem_Int8"(%5, %4) : $Builtin.Int8 // users: %8, %16
debug_value %7, let, name "r", type $UInt8, expr op_fragment:#UInt8._value // id: %8
debug_value %6, let, name "q", type $UInt8, expr op_fragment:#UInt8._value // id: %9
%10 = integer_literal $Builtin.Int8, 48 // users: %16, %12
%11 = integer_literal $Builtin.Int1, 0 // users: %16, %12
%12 = builtin "uadd_with_overflow_Int8"(%6, %10, %11) : $(Builtin.Int8, Builtin.Int1) // user: %13
%13 = tuple_extract %12, 0 // user: %14
%14 = struct $UInt8 (%13) // user: %15
%15 = apply %1(%14) : $@noescape @callee_guaranteed (UInt8) -> ()
%16 = builtin "uadd_with_overflow_Int8"(%7, %10, %11) : $(Builtin.Int8, Builtin.Int1) // user: %17
%17 = tuple_extract %16, 0 // user: %18
%18 = struct $UInt8 (%17) // user: %19
%19 = apply %1(%18) : $@noescape @callee_guaranteed (UInt8) -> ()
%20 = tuple () // user: %21
return %20 // id: %21
} // end sil function 'main.asDecimal(_: Swift.UInt8, writeByte: (Swift.UInt8) -> ()) -> ()'
// viaMutableSpan(_:_:)
// Isolation: unspecified
sil [noinline] @main.viaMutableSpan(Swift.UnsafeMutableRawPointer, Swift.Int) -> Swift.Int : $@convention(thin) (UnsafeMutableRawPointer, Int) -> Int {
[%0: noescape **, read v**.c*.v**, write v**.c*.v**, copy v**.c*.v**, destroy v**.c*.v**]
[global: read,write,copy,destroy,allocate,deinit_barrier]
// %0 "p" // users: %11, %2
// %1 "n" // users: %6, %3
bb0(%0 : $UnsafeMutableRawPointer, %1 : $Int):
debug_value %0, let, name "p", argno 1 // id: %2
debug_value %1, let, name "n", argno 2 // id: %3
%4 = alloc_stack [lexical] [var_decl] $MutableSpan<UInt8>, var, name "span", type $MutableSpan<UInt8> // users: %13, %29, %28, %21, %20, %34
%5 = integer_literal $Builtin.Int64, 0 // users: %15, %7
%6 = struct_extract %1, #Int._value // users: %9, %7
%7 = builtin "cmp_slt_Int64"(%6, %5) : $Builtin.Int1 // user: %8
cond_fail %7, "Count must not be negative" // id: %8
%9 = builtin "assumeNonNegative_Int64"(%6) : $Builtin.Int64 // user: %10
%10 = struct $Int (%9) // user: %12
%11 = enum $Optional<UnsafeMutableRawPointer>, #Optional.some!enumelt, %0 // user: %12
%12 = struct $MutableSpan<UInt8> (%11, %10) // user: %13
store %12 to %4 // id: %13
%14 = alloc_stack [var_decl] $Int, var, name "i", type $Int // users: %32, %16, %28, %20, %33
%15 = struct $Int (%5) // user: %16
store %15 to %14 // id: %16
%17 = integer_literal $Builtin.Int8, 123 // user: %18
%18 = struct $UInt8 (%17) // user: %23
// function_ref closure #1 in viaMutableSpan(_:_:)
%19 = function_ref @closure #1 (Swift.UInt8) -> () in main.viaMutableSpan(Swift.UnsafeMutableRawPointer, Swift.Int) -> Swift.Int : $@convention(thin) (UInt8, @inout_aliasable MutableSpan<UInt8>, @inout_aliasable Int) -> () // user: %20
%20 = partial_apply [callee_guaranteed] [on_stack] %19(%4, %14) : $@convention(thin) (UInt8, @inout_aliasable MutableSpan<UInt8>, @inout_aliasable Int) -> () // users: %24, %21
%21 = mark_dependence [nonescaping] %20 on %4 // user: %23
// function_ref asDecimal(_:writeByte:)
%22 = function_ref @main.asDecimal(_: Swift.UInt8, writeByte: (Swift.UInt8) -> ()) -> () : $@convention(thin) (UInt8, @guaranteed @noescape @callee_guaranteed (UInt8) -> ()) -> () // users: %30, %23
%23 = apply %22(%18, %21) : $@convention(thin) (UInt8, @guaranteed @noescape @callee_guaranteed (UInt8) -> ()) -> ()
dealloc_stack %20 // id: %24
%25 = integer_literal $Builtin.Int8, 45 // user: %26
%26 = struct $UInt8 (%25) // user: %30
// function_ref closure #2 in viaMutableSpan(_:_:)
%27 = function_ref @closure #2 (Swift.UInt8) -> () in main.viaMutableSpan(Swift.UnsafeMutableRawPointer, Swift.Int) -> Swift.Int : $@convention(thin) (UInt8, @inout_aliasable MutableSpan<UInt8>, @inout_aliasable Int) -> () // user: %28
%28 = partial_apply [callee_guaranteed] [on_stack] %27(%4, %14) : $@convention(thin) (UInt8, @inout_aliasable MutableSpan<UInt8>, @inout_aliasable Int) -> () // users: %31, %29
%29 = mark_dependence [nonescaping] %28 on %4 // user: %30
%30 = apply %22(%26, %29) : $@convention(thin) (UInt8, @guaranteed @noescape @callee_guaranteed (UInt8) -> ()) -> ()
dealloc_stack %28 // id: %31
%32 = load %14 // user: %35
dealloc_stack %14 // id: %33
dealloc_stack %4 // id: %34
return %32 // id: %35
} // end sil function 'main.viaMutableSpan(Swift.UnsafeMutableRawPointer, Swift.Int) -> Swift.Int'
// closure #1 in viaMutableSpan(_:_:)
// Isolation: nonisolated
sil private @closure #1 (Swift.UInt8) -> () in main.viaMutableSpan(Swift.UnsafeMutableRawPointer, Swift.Int) -> Swift.Int : $@convention(thin) (UInt8, @inout_aliasable MutableSpan<UInt8>, @inout_aliasable Int) -> () {
[%1: noescape **, read v**]
[%2: read s0.v**, write v**]
[global: read,write,deinit_barrier]
// %0 "$0" // users: %25, %3
// %1 "span" // users: %8, %17, %24, %4
// %2 "i" // users: %32, %6, %5
bb0(%0 : $UInt8, %1 : @closureCapture $*MutableSpan<UInt8>, %2 : @closureCapture $*Int):
debug_value %0, let, name "$0", argno 1 // id: %3
debug_value %1, var, name "span", argno 2, expr op_deref // id: %4
debug_value %2, var, name "i", argno 3, expr op_deref // id: %5
%6 = struct_element_addr %2, #Int._value // users: %27, %7
%7 = load %6 // users: %14, %13, %22
%8 = struct_element_addr %1, #MutableSpan._count // user: %9
%9 = struct_element_addr %8, #Int._value // user: %10
%10 = load %9 // user: %12
%11 = integer_literal $Builtin.Int64, 0 // user: %13
%12 = builtin "assumeNonNegative_Int64"(%10) : $Builtin.Int64 // user: %14
%13 = builtin "cmp_slt_Int64"(%7, %11) : $Builtin.Int1 // user: %15
%14 = builtin "cmp_sge_Int64"(%7, %12) : $Builtin.Int1 // user: %15
%15 = builtin "or_Int1"(%13, %14) : $Builtin.Int1 // user: %16
cond_fail %15, "index out of bounds" // id: %16
%17 = struct_element_addr %1, #MutableSpan._pointer // user: %18
%18 = load %17 // user: %19
%19 = unchecked_enum_data %18, #Optional.some!enumelt // user: %20
%20 = struct_extract %19, #UnsafeMutableRawPointer._rawValue // user: %21
%21 = pointer_to_address %20 to [strict] $*UInt8 // user: %23
%22 = builtin "truncOrBitCast_Int64_Word"(%7) : $Builtin.Word // user: %23
%23 = index_addr %21, %22 // user: %24
%24 = mark_dependence [nonescaping] %23 on %1 // user: %25
store %0 to %24 // id: %25
%26 = integer_literal $Builtin.Int64, 1 // user: %29
%27 = load %6 // user: %29
%28 = integer_literal $Builtin.Int1, 0 // user: %29
%29 = builtin "sadd_with_overflow_Int64"(%27, %26, %28) : $(Builtin.Int64, Builtin.Int1) // user: %30
%30 = tuple_extract %29, 0 // user: %31
%31 = struct $Int (%30) // user: %32
store %31 to %2 // id: %32
%33 = tuple () // user: %34
return %33 // id: %34
} // end sil function 'closure #1 (Swift.UInt8) -> () in main.viaMutableSpan(Swift.UnsafeMutableRawPointer, Swift.Int) -> Swift.Int'
// closure #2 in viaMutableSpan(_:_:)
// Isolation: nonisolated
sil private @closure #2 (Swift.UInt8) -> () in main.viaMutableSpan(Swift.UnsafeMutableRawPointer, Swift.Int) -> Swift.Int : $@convention(thin) (UInt8, @inout_aliasable MutableSpan<UInt8>, @inout_aliasable Int) -> () {
[%1: noescape **, read v**]
[%2: read s0.v**, write v**]
[global: read,write,deinit_barrier]
// %0 "$0" // users: %25, %3
// %1 "span" // users: %8, %17, %24, %4
// %2 "i" // users: %32, %6, %5
bb0(%0 : $UInt8, %1 : @closureCapture $*MutableSpan<UInt8>, %2 : @closureCapture $*Int):
debug_value %0, let, name "$0", argno 1 // id: %3
debug_value %1, var, name "span", argno 2, expr op_deref // id: %4
debug_value %2, var, name "i", argno 3, expr op_deref // id: %5
%6 = struct_element_addr %2, #Int._value // users: %27, %7
%7 = load %6 // users: %14, %13, %22
%8 = struct_element_addr %1, #MutableSpan._count // user: %9
%9 = struct_element_addr %8, #Int._value // user: %10
%10 = load %9 // user: %12
%11 = integer_literal $Builtin.Int64, 0 // user: %13
%12 = builtin "assumeNonNegative_Int64"(%10) : $Builtin.Int64 // user: %14
%13 = builtin "cmp_slt_Int64"(%7, %11) : $Builtin.Int1 // user: %15
%14 = builtin "cmp_sge_Int64"(%7, %12) : $Builtin.Int1 // user: %15
%15 = builtin "or_Int1"(%13, %14) : $Builtin.Int1 // user: %16
cond_fail %15, "index out of bounds" // id: %16
%17 = struct_element_addr %1, #MutableSpan._pointer // user: %18
%18 = load %17 // user: %19
%19 = unchecked_enum_data %18, #Optional.some!enumelt // user: %20
%20 = struct_extract %19, #UnsafeMutableRawPointer._rawValue // user: %21
%21 = pointer_to_address %20 to [strict] $*UInt8 // user: %23
%22 = builtin "truncOrBitCast_Int64_Word"(%7) : $Builtin.Word // user: %23
%23 = index_addr %21, %22 // user: %24
%24 = mark_dependence [nonescaping] %23 on %1 // user: %25
store %0 to %24 // id: %25
%26 = integer_literal $Builtin.Int64, 1 // user: %29
%27 = load %6 // user: %29
%28 = integer_literal $Builtin.Int1, 0 // user: %29
%29 = builtin "sadd_with_overflow_Int64"(%27, %26, %28) : $(Builtin.Int64, Builtin.Int1) // user: %30
%30 = tuple_extract %29, 0 // user: %31
%31 = struct $Int (%30) // user: %32
store %31 to %2 // id: %32
%33 = tuple () // user: %34
return %33 // id: %34
} // end sil function 'closure #2 (Swift.UInt8) -> () in main.viaMutableSpan(Swift.UnsafeMutableRawPointer, Swift.Int) -> Swift.Int'
// viaBuffer(_:_:)
// Isolation: unspecified
sil [noinline] @main.viaBuffer(Swift.UnsafeMutableRawPointer, Swift.Int) -> Swift.Int : $@convention(thin) (UnsafeMutableRawPointer, Int) -> Int {
[%0: noescape **, write s0.v**.c*.v**]
[global: write,deinit_barrier]
// %0 "p" // users: %4, %2
// %1 "n" // user: %3
bb0(%0 : $UnsafeMutableRawPointer, %1 : $Int):
debug_value %0, let, name "p", argno 1 // id: %2
debug_value %1, let, name "n", argno 2 // id: %3
%4 = struct_extract %0, #UnsafeMutableRawPointer._rawValue // user: %5
%5 = struct $UnsafeMutablePointer<UInt8> (%4) // user: %6
%6 = enum $Optional<UnsafeMutablePointer<UInt8>>, #Optional.some!enumelt, %5 // users: %7, %19, %15
debug_value %6, let, name "buf", type $UnsafeMutableBufferPointer<UInt8>, expr op_fragment:#UnsafeMutableBufferPointer._position // id: %7
%8 = alloc_stack [var_decl] $Int, var, name "i", type $Int // users: %15, %19, %20, %11, %21
%9 = integer_literal $Builtin.Int64, 0 // user: %10
%10 = struct $Int (%9) // user: %11
store %10 to %8 // id: %11
%12 = integer_literal $Builtin.Int8, 123 // user: %13
%13 = struct $UInt8 (%12) // user: %15
// function_ref specialized asDecimal(_:writeByte:)
%14 = function_ref @function signature specialization <Arg[1] = Exploded> of function signature specialization <Arg[1] = [Closure Propagated : $s4main9viaBufferySiSv_SitFys5UInt8VXEfU_, Argument Types : [Swift.UnsafeMutableBufferPointer<Swift.UInt8>Swift.Int]> of main.asDecimal(_: Swift.UInt8, writeByte: (Swift.UInt8) -> ()) -> () : $@convention(thin) (UInt8, Optional<UnsafeMutablePointer<UInt8>>, @inout_aliasable Int) -> () // user: %15
%15 = apply %14(%13, %6, %8) : $@convention(thin) (UInt8, Optional<UnsafeMutablePointer<UInt8>>, @inout_aliasable Int) -> ()
%16 = integer_literal $Builtin.Int8, 45 // user: %17
%17 = struct $UInt8 (%16) // user: %19
// function_ref specialized asDecimal(_:writeByte:)
%18 = function_ref @function signature specialization <Arg[1] = Exploded> of function signature specialization <Arg[1] = [Closure Propagated : $s4main9viaBufferySiSv_SitFys5UInt8VXEfU0_, Argument Types : [Swift.UnsafeMutableBufferPointer<Swift.UInt8>Swift.Int]> of main.asDecimal(_: Swift.UInt8, writeByte: (Swift.UInt8) -> ()) -> () : $@convention(thin) (UInt8, Optional<UnsafeMutablePointer<UInt8>>, @inout_aliasable Int) -> () // user: %19
%19 = apply %18(%17, %6, %8) : $@convention(thin) (UInt8, Optional<UnsafeMutablePointer<UInt8>>, @inout_aliasable Int) -> ()
%20 = load %8 // user: %22
dealloc_stack %8 // id: %21
return %20 // id: %22
} // end sil function 'main.viaBuffer(Swift.UnsafeMutableRawPointer, Swift.Int) -> Swift.Int'
// specialized asDecimal(_:writeByte:)
// Isolation: unspecified
sil shared [noinline] @function signature specialization <Arg[1] = Exploded> of function signature specialization <Arg[1] = [Closure Propagated : $s4main9viaBufferySiSv_SitFys5UInt8VXEfU_, Argument Types : [Swift.UnsafeMutableBufferPointer<Swift.UInt8>Swift.Int]> of main.asDecimal(_: Swift.UInt8, writeByte: (Swift.UInt8) -> ()) -> () : $@convention(thin) (UInt8, Optional<UnsafeMutablePointer<UInt8>>, @inout_aliasable Int) -> () {
[%1: noescape **, write v**.c*.v**]
[%2: read s0.v**, write v**]
[global: write,deinit_barrier]
// %0 // users: %5, %3
// %1 // user: %19
// %2 // users: %30, %17, %16, %43, %35
bb0(%0 : $UInt8, %1 : $Optional<UnsafeMutablePointer<UInt8>>, %2 : $*Int):
debug_value %0, let, name "x", argno 1 // id: %3
%4 = integer_literal $Builtin.Int8, 10 // users: %7, %6
%5 = struct_extract %0, #UInt8._value // users: %7, %6
%6 = builtin "udiv_Int8"(%5, %4) : $Builtin.Int8 // users: %12, %9
%7 = builtin "urem_Int8"(%5, %4) : $Builtin.Int8 // users: %31, %8
debug_value %7, let, name "r", type $UInt8, expr op_fragment:#UInt8._value // id: %8
debug_value %6, let, name "q", type $UInt8, expr op_fragment:#UInt8._value // id: %9
%10 = integer_literal $Builtin.Int8, 48 // users: %31, %12
%11 = integer_literal $Builtin.Int1, 0 // users: %40, %27, %31, %12
%12 = builtin "uadd_with_overflow_Int8"(%6, %10, %11) : $(Builtin.Int8, Builtin.Int1) // user: %13
%13 = tuple_extract %12, 0 // user: %14
%14 = struct $UInt8 (%13) // users: %24, %15
debug_value %14, let, name "$0", argno 1 // id: %15
debug_value %2, var, name "i", argno 3, expr op_deref // id: %16
%17 = struct_element_addr %2, #Int._value // users: %39, %26, %18
%18 = load %17 // user: %21
%19 = unchecked_enum_data %1, #Optional.some!enumelt // user: %20
%20 = struct_extract %19, #UnsafeMutablePointer._rawValue // user: %22
%21 = builtin "truncOrBitCast_Int64_Word"(%18) : $Builtin.Word // user: %23
%22 = pointer_to_address %20 to [strict] $*UInt8 // users: %37, %23
%23 = index_addr [stack_protection] %22, %21 // user: %24
store %14 to %23 // id: %24
%25 = integer_literal $Builtin.Int64, 1 // users: %40, %27
%26 = load %17 // user: %27
%27 = builtin "sadd_with_overflow_Int64"(%26, %25, %11) : $(Builtin.Int64, Builtin.Int1) // user: %28
%28 = tuple_extract %27, 0 // users: %36, %29
%29 = struct $Int (%28) // user: %30
store %29 to %2 // id: %30
%31 = builtin "uadd_with_overflow_Int8"(%7, %10, %11) : $(Builtin.Int8, Builtin.Int1) // user: %32
%32 = tuple_extract %31, 0 // user: %33
%33 = struct $UInt8 (%32) // users: %38, %34
debug_value %33, let, name "$0", argno 1 // id: %34
debug_value %2, var, name "i", argno 3, expr op_deref // id: %35
%36 = builtin "truncOrBitCast_Int64_Word"(%28) : $Builtin.Word // user: %37
%37 = index_addr [stack_protection] %22, %36 // user: %38
store %33 to %37 // id: %38
%39 = load %17 // user: %40
%40 = builtin "sadd_with_overflow_Int64"(%39, %25, %11) : $(Builtin.Int64, Builtin.Int1) // user: %41
%41 = tuple_extract %40, 0 // user: %42
%42 = struct $Int (%41) // user: %43
store %42 to %2 // id: %43
%44 = tuple () // user: %45
return %44 // id: %45
} // end sil function 'function signature specialization <Arg[1] = Exploded> of function signature specialization <Arg[1] = [Closure Propagated : $s4main9viaBufferySiSv_SitFys5UInt8VXEfU_, Argument Types : [Swift.UnsafeMutableBufferPointer<Swift.UInt8>Swift.Int]> of main.asDecimal(_: Swift.UInt8, writeByte: (Swift.UInt8) -> ()) -> ()'
// specialized asDecimal(_:writeByte:)
// Isolation: unspecified
sil shared [noinline] @function signature specialization <Arg[1] = Exploded> of function signature specialization <Arg[1] = [Closure Propagated : $s4main9viaBufferySiSv_SitFys5UInt8VXEfU0_, Argument Types : [Swift.UnsafeMutableBufferPointer<Swift.UInt8>Swift.Int]> of main.asDecimal(_: Swift.UInt8, writeByte: (Swift.UInt8) -> ()) -> () : $@convention(thin) (UInt8, Optional<UnsafeMutablePointer<UInt8>>, @inout_aliasable Int) -> () {
[%1: noescape **, write v**.c*.v**]
[%2: read s0.v**, write v**]
[global: write,deinit_barrier]
// %0 // users: %5, %3
// %1 // user: %19
// %2 // users: %30, %17, %16, %43, %35
bb0(%0 : $UInt8, %1 : $Optional<UnsafeMutablePointer<UInt8>>, %2 : $*Int):
debug_value %0, let, name "x", argno 1 // id: %3
%4 = integer_literal $Builtin.Int8, 10 // users: %7, %6
%5 = struct_extract %0, #UInt8._value // users: %7, %6
%6 = builtin "udiv_Int8"(%5, %4) : $Builtin.Int8 // users: %12, %9
%7 = builtin "urem_Int8"(%5, %4) : $Builtin.Int8 // users: %31, %8
debug_value %7, let, name "r", type $UInt8, expr op_fragment:#UInt8._value // id: %8
debug_value %6, let, name "q", type $UInt8, expr op_fragment:#UInt8._value // id: %9
%10 = integer_literal $Builtin.Int8, 48 // users: %31, %12
%11 = integer_literal $Builtin.Int1, 0 // users: %40, %27, %31, %12
%12 = builtin "uadd_with_overflow_Int8"(%6, %10, %11) : $(Builtin.Int8, Builtin.Int1) // user: %13
%13 = tuple_extract %12, 0 // user: %14
%14 = struct $UInt8 (%13) // users: %24, %15
debug_value %14, let, name "$0", argno 1 // id: %15
debug_value %2, var, name "i", argno 3, expr op_deref // id: %16
%17 = struct_element_addr %2, #Int._value // users: %39, %26, %18
%18 = load %17 // user: %21
%19 = unchecked_enum_data %1, #Optional.some!enumelt // user: %20
%20 = struct_extract %19, #UnsafeMutablePointer._rawValue // user: %22
%21 = builtin "truncOrBitCast_Int64_Word"(%18) : $Builtin.Word // user: %23
%22 = pointer_to_address %20 to [strict] $*UInt8 // users: %37, %23
%23 = index_addr [stack_protection] %22, %21 // user: %24
store %14 to %23 // id: %24
%25 = integer_literal $Builtin.Int64, 1 // users: %40, %27
%26 = load %17 // user: %27
%27 = builtin "sadd_with_overflow_Int64"(%26, %25, %11) : $(Builtin.Int64, Builtin.Int1) // user: %28
%28 = tuple_extract %27, 0 // users: %36, %29
%29 = struct $Int (%28) // user: %30
store %29 to %2 // id: %30
%31 = builtin "uadd_with_overflow_Int8"(%7, %10, %11) : $(Builtin.Int8, Builtin.Int1) // user: %32
%32 = tuple_extract %31, 0 // user: %33
%33 = struct $UInt8 (%32) // users: %38, %34
debug_value %33, let, name "$0", argno 1 // id: %34
debug_value %2, var, name "i", argno 3, expr op_deref // id: %35
%36 = builtin "truncOrBitCast_Int64_Word"(%28) : $Builtin.Word // user: %37
%37 = index_addr [stack_protection] %22, %36 // user: %38
store %33 to %37 // id: %38
%39 = load %17 // user: %40
%40 = builtin "sadd_with_overflow_Int64"(%39, %25, %11) : $(Builtin.Int64, Builtin.Int1) // user: %41
%41 = tuple_extract %40, 0 // user: %42
%42 = struct $Int (%41) // user: %43
store %42 to %2 // id: %43
%44 = tuple () // user: %45
return %44 // id: %45
} // end sil function 'function signature specialization <Arg[1] = Exploded> of function signature specialization <Arg[1] = [Closure Propagated : $s4main9viaBufferySiSv_SitFys5UInt8VXEfU0_, Argument Types : [Swift.UnsafeMutableBufferPointer<Swift.UInt8>Swift.Int]> of main.asDecimal(_: Swift.UInt8, writeByte: (Swift.UInt8) -> ()) -> ()'
// Mappings from '#fileID' to '#filePath':
// 'main/issue-89954.swift' => 'issue-89954.swift'
```
</details>
You can see that `viaMutableSpan`'s SIL is as follows:
```bash
// viaMutableSpan(_:_:)
// Isolation: unspecified
sil [noinline] @main.viaMutableSpan(Swift.UnsafeMutableRawPointer, Swift.Int) -> Swift.Int : $@convention(thin) (UnsafeMutableRawPointer, Int) -> Int {
[%0: noescape **, read v**.c*.v**, write v**.c*.v**, copy v**.c*.v**, destroy v**.c*.v**]
[global: read,write,copy,destroy,allocate,deinit_barrier]
// %0 "p" // users: %11, %2
// %1 "n" // users: %6, %3
bb0(%0 : $UnsafeMutableRawPointer, %1 : $Int):
debug_value %0, let, name "p", argno 1 // id: %2
debug_value %1, let, name "n", argno 2 // id: %3
%4 = alloc_stack [lexical] [var_decl] $MutableSpan<UInt8>, var, name "span", type $MutableSpan<UInt8> // users: %13, %29, %28, %21, %20, %34
%5 = integer_literal $Builtin.Int64, 0 // users: %15, %7
%6 = struct_extract %1, #Int._value // users: %9, %7
%7 = builtin "cmp_slt_Int64"(%6, %5) : $Builtin.Int1 // user: %8
cond_fail %7, "Count must not be negative" // id: %8
%9 = builtin "assumeNonNegative_Int64"(%6) : $Builtin.Int64 // user: %10
%10 = struct $Int (%9) // user: %12
%11 = enum $Optional<UnsafeMutableRawPointer>, #Optional.some!enumelt, %0 // user: %12
%12 = struct $MutableSpan<UInt8> (%11, %10) // user: %13
store %12 to %4 // id: %13
%14 = alloc_stack [var_decl] $Int, var, name "i", type $Int // users: %32, %16, %28, %20, %33
%15 = struct $Int (%5) // user: %16
store %15 to %14 // id: %16
%17 = integer_literal $Builtin.Int8, 123 // user: %18
%18 = struct $UInt8 (%17) // user: %23
// function_ref closure #1 in viaMutableSpan(_:_:)
%19 = function_ref @closure #1 (Swift.UInt8) -> () in main.viaMutableSpan(Swift.UnsafeMutableRawPointer, Swift.Int) -> Swift.Int : $@convention(thin) (UInt8, @inout_aliasable MutableSpan<UInt8>, @inout_aliasable Int) -> () // user: %20
%20 = partial_apply [callee_guaranteed] [on_stack] %19(%4, %14) : $@convention(thin) (UInt8, @inout_aliasable MutableSpan<UInt8>, @inout_aliasable Int) -> () // users: %24, %21
%21 = mark_dependence [nonescaping] %20 on %4 // user: %23
// function_ref asDecimal(_:writeByte:)
%22 = function_ref @main.asDecimal(_: Swift.UInt8, writeByte: (Swift.UInt8) -> ()) -> () : $@convention(thin) (UInt8, @guaranteed @noescape @callee_guaranteed (UInt8) -> ()) -> () // users: %30, %23
%23 = apply %22(%18, %21) : $@convention(thin) (UInt8, @guaranteed @noescape @callee_guaranteed (UInt8) -> ()) -> ()
dealloc_stack %20 // id: %24
%25 = integer_literal $Builtin.Int8, 45 // user: %26
%26 = struct $UInt8 (%25) // user: %30
// function_ref closure #2 in viaMutableSpan(_:_:)
%27 = function_ref @closure #2 (Swift.UInt8) -> () in main.viaMutableSpan(Swift.UnsafeMutableRawPointer, Swift.Int) -> Swift.Int : $@convention(thin) (UInt8, @inout_aliasable MutableSpan<UInt8>, @inout_aliasable Int) -> () // user: %28
%28 = partial_apply [callee_guaranteed] [on_stack] %27(%4, %14) : $@convention(thin) (UInt8, @inout_aliasable MutableSpan<UInt8>, @inout_aliasable Int) -> () // users: %31, %29
%29 = mark_dependence [nonescaping] %28 on %4 // user: %30
%30 = apply %22(%26, %29) : $@convention(thin) (UInt8, @guaranteed @noescape @callee_guaranteed (UInt8) -> ()) -> ()
dealloc_stack %28 // id: %31
%32 = load %14 // user: %35
dealloc_stack %14 // id: %33
dealloc_stack %4 // id: %34
return %32 // id: %35
} // end sil function 'main.viaMutableSpan(Swift.UnsafeMutableRawPointer, Swift.Int) -> Swift.Int'
```
And for `viaBuffer`:
```bash
// viaBuffer(_:_:)
// Isolation: unspecified
sil [noinline] @main.viaBuffer(Swift.UnsafeMutableRawPointer, Swift.Int) -> Swift.Int : $@convention(thin) (UnsafeMutableRawPointer, Int) -> Int {
[%0: noescape **, write s0.v**.c*.v**]
[global: write,deinit_barrier]
// %0 "p" // users: %4, %2
// %1 "n" // user: %3
bb0(%0 : $UnsafeMutableRawPointer, %1 : $Int):
debug_value %0, let, name "p", argno 1 // id: %2
debug_value %1, let, name "n", argno 2 // id: %3
%4 = struct_extract %0, #UnsafeMutableRawPointer._rawValue // user: %5
%5 = struct $UnsafeMutablePointer<UInt8> (%4) // user: %6
%6 = enum $Optional<UnsafeMutablePointer<UInt8>>, #Optional.some!enumelt, %5 // users: %7, %19, %15
debug_value %6, let, name "buf", type $UnsafeMutableBufferPointer<UInt8>, expr op_fragment:#UnsafeMutableBufferPointer._position // id: %7
%8 = alloc_stack [var_decl] $Int, var, name "i", type $Int // users: %15, %19, %20, %11, %21
%9 = integer_literal $Builtin.Int64, 0 // user: %10
%10 = struct $Int (%9) // user: %11
store %10 to %8 // id: %11
%12 = integer_literal $Builtin.Int8, 123 // user: %13
%13 = struct $UInt8 (%12) // user: %15
// function_ref specialized asDecimal(_:writeByte:)
%14 = function_ref @function signature specialization <Arg[1] = Exploded> of function signature specialization <Arg[1] = [Closure Propagated : $s4main9viaBufferySiSv_SitFys5UInt8VXEfU_, Argument Types : [Swift.UnsafeMutableBufferPointer<Swift.UInt8>Swift.Int]> of main.asDecimal(_: Swift.UInt8, writeByte: (Swift.UInt8) -> ()) -> () : $@convention(thin) (UInt8, Optional<UnsafeMutablePointer<UInt8>>, @inout_aliasable Int) -> () // user: %15
%15 = apply %14(%13, %6, %8) : $@convention(thin) (UInt8, Optional<UnsafeMutablePointer<UInt8>>, @inout_aliasable Int) -> ()
%16 = integer_literal $Builtin.Int8, 45 // user: %17
%17 = struct $UInt8 (%16) // user: %19
// function_ref specialized asDecimal(_:writeByte:)
%18 = function_ref @function signature specialization <Arg[1] = Exploded> of function signature specialization <Arg[1] = [Closure Propagated : $s4main9viaBufferySiSv_SitFys5UInt8VXEfU0_, Argument Types : [Swift.UnsafeMutableBufferPointer<Swift.UInt8>Swift.Int]> of main.asDecimal(_: Swift.UInt8, writeByte: (Swift.UInt8) -> ()) -> () : $@convention(thin) (UInt8, Optional<UnsafeMutablePointer<UInt8>>, @inout_aliasable Int) -> () // user: %19
%19 = apply %18(%17, %6, %8) : $@convention(thin) (UInt8, Optional<UnsafeMutablePointer<UInt8>>, @inout_aliasable Int) -> ()
%20 = load %8 // user: %22
dealloc_stack %8 // id: %21
return %20 // id: %22
} // end sil function 'main.viaBuffer(Swift.UnsafeMutableRawPointer, Swift.Int) -> Swift.Int'
```
One difference is that in `viaMutableSpan` we have:
```bash
function_ref closure #1 in viaSpan(_:_:)
```
But in `viaBuffer`:
```bash
function_ref specialized asDecimal(_:writeByte:)
```
So clearly, in `viaMutableSpan`, the `asDecimal(_:writeByte:)` function fails to get specialized/inlined.
This then disables some optimizations of the compiler, which result in a relatively considerable amount of lower performance for `MutableSpan`.
In Claude's words:
> Both functions are the same shape; the only difference is the captured type. MutableSpan (~Escapable) ⇒ mark_dependence ⇒ optimizer gives up ⇒ generic call with an indirect closure call. UnsafeMutableBufferPointer (Copyable) ⇒ no mark_dependence ⇒ optimizer specializes ⇒ direct call. That contrast is the bug.
### The solution
I'm not a compiler engineer so the changes are made by Claude. I did persuade it a lot to try to make sure it has made proper changes, but I can't be sure that it actually did (well, apart from the fact that the changes are indeed working, since I asked Claude to compile and verify as well even though it takes a while to compile):
```md
### Problem
findSpecializableClosure's MarkDependenceInst case bailed unless visited.contains(mdi.base). A non-escaping closure capturing a ~Escapable value has a mark_dependence whose base is a capture (not in the chain), so it never specialized — leaving a generic callee with a per-call indirect apply.
### Fix
Accept when the base is a captured operand of the root closure (computed lazily, so it's only evaluated when the visited check fails):
var baseIsRootClosureCapture: Bool {
(operandClosure as? PartialApplyInst)?.arguments.contains { $0 == mdi.base } ?? false
}
Existing gates still run via the recursion into mdi.value.
Resolve the base into the specialization. uniqueCaptureArguments rewrites the partial_apply operands to identity casts but not the mark_dependence base, so mdi.base == cast.fromValue. visitMarkDependenceInst resolves the base through the cloner map, so fold the pre-cast value too:
if let cast = originalClosureArg as? UncheckedValueCastInst,
!cloner.isCloned(value: cast.fromValue) {
cloner.recordFoldedValue(cast.fromValue, mappedTo: capturedArg)
}
Guard: the cloner map is value-keyed, so bail if a base is captured >1× (otherwise a reused specialization mis-maps). hasMultiplyCapturedDependenceBase walks the closure-argument chain (mirroring findSpecializableClosure, including reabstraction thunks) and short-circuits via isCapturedMoreThanOnce — no value accumulation.
### Soundness
Gated on isNoEscapeFunction ⇒ captures are forwarded @guaranteed and outlive the apply; the mark_dependence is re-materialized on the new argument. Every accepted base is either cloned (in chain) or a uniquely-captured value folded to its arg.
### Tests
closure_specialization_attrs.sil updated (on-stack [readonly] now specializes, @guaranteed, no retain). Full test/SILOptimizer green (1167 passed, 0 failures); standard library rebuilds under -enable-sil-verify-all with no optimizer crash; the reproducer specializes (no residual partial_apply/mark_dependence).
```
### Environment
```bash
$ swiftc --version
swift-driver version: 1.167 Apple Swift version 6.4 (swiftlang-6.4.0.20.104 clang-2100.3.20.102)
Target: arm64-apple-macosx27.0.0
```