diff --git a/bench/ffi/README.md b/bench/ffi/README.md index 989c3fc00..957020807 100644 --- a/bench/ffi/README.md +++ b/bench/ffi/README.md @@ -155,19 +155,24 @@ frame path: C ABI's borrow of each handle, a compare-and-swap in and a `fetch_sub` out of its state, is the whole call. Every handle argument pays it again. 7. **The binding's own call.** Over C's getter: Swift adds 8 ns (its hold count's two - atomics), Kotlin 14 wall (JNI and its hold count), Dart 19 (an `_Arena` per call and + atomics, since dropped from all but its classes, as below), Kotlin 14 wall (JNI and its hold count), Dart 19 (an `_Arena` per call and its hold), Go 31 (cgo, `defer`, and its hold), and Python 37 (argument parsing and the GIL released and taken back, so Rust may call Python from another thread). BoltFFI's Python getter, which holds the GIL, measured 63 ns. ### Per-handle baseline -For [checks-raise](/quest/m1/ffi-export/checks-raise.md)'s one accounting and -[swift-noncopyable](/quest/m1/ffi-export/swift-noncopyable.md)'s hold count: the u64 +For [checks-raise](/quest/m1/ffi-export/checks-raise.md)'s one accounting: the u64 getter is one borrowed handle and nothing else, so its row less Rust's 0.4 ns is what a handle costs a call. C, C++: 10 ns, the runtime's two atomics. Swift: 18, its hold count adding two more. Kotlin: 24 wall, Go 41, Dart 29, Python 47, each with its call's own -transition besides. A change to the accounting moves this row and the handle call's by the +transition besides. + +Swift has since made each object a noncopyable struct and dropped its hold count, but for +a class: a `Copy` object or one thrown as an error. Measured 2026-11-11 at that change, 5 +rounds interleaved with C at a load average of 7 to 12, its getter costs 10 ns, as C's +does, and `track.is_closed()`, on `TrackProducer`, still a class since moq throws it, 26 +against C's 18, as in the table. A change to the accounting moves this row and the handle call's by the same amount; measure both, CPU first, at the load beside them. ### Against the targets diff --git a/ffi/README.md b/ffi/README.md index 8fa734fac..1bdfb9bd1 100644 --- a/ffi/README.md +++ b/ffi/README.md @@ -484,22 +484,35 @@ caller imports it with no ignores. the header as the clang module `FFI`, which `swift/module.modulemap` declares beside `swift/_atomics.h`, the atomics its runtime needs that Swift's standard library has only from macOS 15: a build puts `swift/` on its import path and links the -static library. It needs Swift 6 and, for `Swift.Duration`, macOS 13 or iOS 16, and builds -in either language mode. +static library. It imports swift-collections' `BasicContainers` for `UniqueArray`, which +`just ffi swift-collections` builds at the version the tests pin, 1.7.5. It needs Swift 6.2 +and, for `Swift.Duration`, macOS 13 or iOS 16, and builds in either language mode. - **Names** are as Kotlin's, at the module's top level: types in PascalCase without the prefix, members in camelCase without their type's name (`TrackProducer.appendGroup`). A keyword is quoted in backticks, and a member named as one Swift gives its type, such as `init`, or as a field or member before it, takes a trailing `_`. Names that clash are - kept apart as Kotlin's are. Parameters have no labels, so a call reads as Rust's, and a + kept apart as Kotlin's are. Parameters have no labels but an optional borrowed object's, + so a call reads as Rust's, and a constructor such as `new` stays a static function: `Counter.new(1)`. -- **Objects** are final classes owning their handle, freed in `deinit`, which a call - holds off until it returns, so there is no `close()`. A `self` method, or passing the - object by value, moves its value out and closes it once the call returns, after which a - call traps. `Clone` is `clone()`, and `PartialEq` - is `==`. A field is a computed property, `firstGet`, and its setter a function, - `firstSet(value)`, named as the C ones, since setting an object moves it in and closes - its handle, which an assignment would hide. An object is `Sendable`, since Rust checks each borrow at run time. +- **Objects** are noncopyable structs (`~Copyable`) owning their handle, freed in `deinit`, + so there is no `close()`, and Swift's compiler checks each move and borrow Rust makes: a + `self` method is `consuming`, a `&mut self` one `mutating`, so its object must be a + `var`, and a parameter is `consuming` by value, `borrowing` for `&T`, and `inout` for + `&mut T`. A call on a moved object, or an object lent to a closure kept past its call, + fails to compile. A tuple holding one is a generated struct, `TupleStringCounter`, whose + fields `_0`, `_1`, ... are its elements, and an array of them swift-collections' + `UniqueArray`. Since an optional holds one only by consuming it, an `Option<&T>` of one + is an overload with and without it, labelled by its name, as a defaulted argument is: + `clamped()`, `clamped(low:)`, `clamped(high:)` and `clamped(low:high:)`, up to two per + function; `Option<&mut T>` is `inout T?`. `Clone` is `clone()`, and `PartialEq` is `==`, which Swift 6.4 makes + `Equatable`; `Display` is `description` and `Debug` `debugDescription`, which Swift + conforms no noncopyable type to yet. An object that is `Copy`, or thrown as an error, + which `Swift.Error` requires be copyable, is a final class instead, whose handle a call + holds until it returns, and moving its value out closes it, after which a call traps. A + field is a computed property, `firstGet`, and its setter a function, `firstSet(value)`, + named as the C ones, since setting an object moves it in, which an assignment would hide. + An object is `Sendable`, since Rust checks each borrow at run time. - **Records** are structs and **enums** Swift enums, with a variant's data as associated values, all `Sendable`. `PartialEq` is `==`, through Rust's; `Display` is `description`, `Debug` `debugDescription`, and `Default` a static `default()`. @@ -517,10 +530,12 @@ in either language mode. - A **path** is a `String`: one Rust returns that is not UTF-8 traps. A `SocketAddr`, `SocketAddrV4`, or `SocketAddrV6` is the runtime's `SocketAddr`, its IP's 4 or 16 bytes and its port, flow label, and scope, whose family Rust checks. A `Vec` or `&[T]` is - an array, and passing a `Vec` by value closes each object in it once the call returns. + an array, or a `UniqueArray` of objects. - **Traits** are `Sendable` protocols, which Rust releases once it drops an implementation. - **Closures** lent for a call are `@Sendable` function types, which Rust calls only during - the call, from any thread. An object it lends is closed once the call returns. + the call, from any thread. An object it lends is `borrowing`, or `inout` for `&mut T`, + so it cannot outlive the call; assigning another to the `inout` one traps, since Rust's + stays its own. - **Futures** are `async throws` functions, which resume on the task's executor, never inside a wake. Cancelling the task frees the future, losing nothing, and throws `CancellationError`. Each call is its own future, so it has one poller. The call polls it @@ -781,7 +796,8 @@ abort with its message; the Python with another contract must refuse the library `just ffi moq` checks moq's. `tests/api.swift` runs the script through the Swift, built as a module in Swift 6's language mode and type-checked in Swift 5's, checks its futures the same way and that -releasing frees, and breaks each runtime check, which must trap or abort with its message. +releasing frees, and breaks each runtime check, which must trap or abort with its message, +and each move or borrow the compiler checks, which must fail to compile. It runs with nixpkgs' Swift on Linux, which builds none for aarch64-linux ([quest](/quest/m2/swift-arm.md)), and nightly with Apple's on macOS. `tests/api.go` runs the script through the Go after `go vet`, checks its futures the same diff --git a/ffi/fixtures/api/src/lib.rs b/ffi/fixtures/api/src/lib.rs index a1099be16..9ddd5a22b 100644 --- a/ffi/fixtures/api/src/lib.rs +++ b/ffi/fixtures/api/src/lib.rs @@ -164,6 +164,13 @@ impl Counter { self.value + other.map_or(0, |other| other.value) } + /// This count, raised to `low`'s and lowered to `high`'s, each if any: two optional + /// handles, lent shared. + pub fn clamped(&self, low: Option<&Counter>, high: Option<&Counter>) -> u64 { + let low = low.map_or(self.value, |low| self.value.max(low.value)); + high.map_or(low, |high| low.min(high.value)) + } + /// [`Self::plus`], once [`yielded`] finishes: a future borrowing both handles meanwhile. pub async fn plus_later(&self, other: Option<&Counter>) -> u64 { yielded(0).await; @@ -2304,6 +2311,16 @@ impl fmt::Display for Refusal { } } +/// Each counter's name with its refusal, `last`'s too if any: an object Swift holds as a +/// struct beside one it holds as a class, moved in inside a list and an option. +pub fn reasons(pairs: Vec<(Counter, Refusal)>, last: Option<(Counter, Refusal)>) -> String { + let pairs = pairs.iter().chain(&last); + pairs + .map(|(counter, refusal)| format!("{} {refusal}", counter.name)) + .collect::>() + .join(", ") +} + /// Always refuses, for `reason`. pub fn refuse(reason: &str) -> Result> { Err(Box::new(Refusal { diff --git a/ffi/fixtures/reject/expected.txt b/ffi/fixtures/reject/expected.txt index 32bc7ab71..54886b904 100644 --- a/ffi/fixtures/reject/expected.txt +++ b/ffi/fixtures/reject/expected.txt @@ -103,5 +103,7 @@ fixtures/reject/src/lib.rs:855:9: rust2-ffi: cannot export use `renamed`: re-exp fixtures/reject/src/lib.rs:866:2: rust2-ffi: cannot export method `Tally::poll_bump`: `&mut self` of a value; exclude it in the config to leave it out fixtures/reject/src/lib.rs:886:1: rust2-ffi: cannot export fn `poll_risky`: Poll with no Context or waiter parameter `std::task::Poll<()>`; exclude it in the config to leave it out fixtures/reject/src/lib.rs:902:1: rust2-ffi: cannot export fn `poll_loose`: Poll with no Context or waiter parameter `std::task::Poll<()>`; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:908:1: rust2-ffi: cannot export fn `sum_pair`: array of an element other than u8 `[u16; 2]`; exclude it in the config to leave it out -fixtures/reject/src/lib.rs:913:1: rust2-ffi: cannot export fn `empty`: empty array `[u8; 0]`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:909:1: rust2-ffi: cannot export fn `among`: 3 optional borrowed objects, past the 2 Swift takes, which overloads each with and without it since its optional holds an object only by consuming it; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:915:1: rust2-ffi: cannot export fn `wrapped`: error holding an object Swift cannot copy, as `Swift.Error` requires; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:920:1: rust2-ffi: cannot export fn `sum_pair`: array of an element other than u8 `[u16; 2]`; exclude it in the config to leave it out +fixtures/reject/src/lib.rs:925:1: rust2-ffi: cannot export fn `empty`: empty array `[u8; 0]`; exclude it in the config to leave it out diff --git a/ffi/fixtures/reject/src/lib.rs b/ffi/fixtures/reject/src/lib.rs index 0d83fd5be..05ad2a99a 100644 --- a/ffi/fixtures/reject/src/lib.rs +++ b/ffi/fixtures/reject/src/lib.rs @@ -904,6 +904,18 @@ pub fn poll_loose(waiter: &Loose) -> std::task::Poll<()> { std::task::Poll::Ready(()) } +/// Three optional borrowed objects: Swift, whose optional holds an object only by consuming +/// it, would overload each with and without it, past the bound it takes. +pub fn among(first: Option<&Opaque>, second: Option<&Opaque>, third: Option<&Opaque>) -> u8 { + [first, second, third].into_iter().flatten().map(|o| o.0).sum() +} + +/// An error holding an object, which Swift holds as a noncopyable struct while +/// `Swift.Error` requires a copyable type. +pub fn wrapped(value: u8) -> Result> { + Err(vec![Opaque(value)]) +} + /// An array of an element other than `u8`, which no export needs yet. pub fn sum_pair(pair: [u16; 2]) -> u32 { u32::from(pair[0]) + u32::from(pair[1]) diff --git a/ffi/justfile b/ffi/justfile index b717277f7..4e77ec0d0 100644 --- a/ffi/justfile +++ b/ffi/justfile @@ -14,6 +14,9 @@ export PATH := if env("RUST2_TS_TOOLCHAIN", "") == "" { env("PATH") } else { car # just's own path is quoted wherever bash runs it: on Windows, its backslashes are escapes. moq_rev := shell(quote(just_executable()) + " --justfile ../ts/justfile --evaluate moq_rev") +# swift-collections 1.7.5, whose `UniqueArray` the generated Swift imports. +swift_collections_rev := "f96b909e1a9e469620bc5037c1e24e00153f376f" + default: @just --list @@ -82,7 +85,7 @@ bench-build checkout="": (moq-export checkout) rm -f target/bench-export-swift else {{ quote(just_executable()) }} swift-module -O - swiftc -O -warnings-as-errors $({{ quote(just_executable()) }} swift-target 15) -I target/swift -I "$out/swift" moq/bench/export.swift target/swift/libMoq.a "$out/target/release/libmoq.a" -lpthread -ldl -lm -o target/bench-export-swift + swiftc -O -warnings-as-errors $({{ quote(just_executable()) }} swift-target 15) -I target/swift -I target/swift-collections/build -I "$out/swift" moq/bench/export.swift target/swift/libMoq.a -L target/swift-collections/build -lBasicContainers -lInternalCollectionsUtilities -lSpanPreview "$out/target/release/libmoq.a" -lpthread -ldl -lm -o target/bench-export-swift fi # Run what bench-build built: each program `rounds` times into target/bench/raw.tsv, with the 1-minute load average over each run, and their medians and the export's sizes into target/bench/report.md. @@ -185,18 +188,47 @@ moq-swift-run: out=target/export/moq read -ra native < target/moq-native {{ quote(just_executable()) }} swift-module - swiftc -warnings-as-errors $({{ quote(just_executable()) }} swift-target 15) -parse-as-library -I target/swift -I "$out/swift" moq/moq.swift target/swift/libMoq.a "$out/target/release/libmoq.a" "${native[@]}" -o target/moq-swift-test + swiftc -warnings-as-errors $({{ quote(just_executable()) }} swift-target 15) -parse-as-library -I target/swift -I target/swift-collections/build -I "$out/swift" moq/moq.swift target/swift/libMoq.a -L target/swift-collections/build -lBasicContainers -lInternalCollectionsUtilities -lSpanPreview "$out/target/release/libmoq.a" "${native[@]}" -o target/moq-swift-test target/moq-swift-test # Build target/export/moq's Swift as the module Moq in target/swift, at Swift 6's language mode. Its deployment target is the oldest its `Swift.Duration` allows. [private] -swift-module *flags: +swift-module *flags: swift-collections #!/usr/bin/env bash set -euo pipefail out=target/export/moq/swift rm -rf target/swift mkdir -p target/swift - swiftc -warnings-as-errors $({{ quote(just_executable()) }} swift-target 13) -swift-version 6 {{ flags }} -parse-as-library -module-name Moq -emit-module -emit-module-path target/swift/Moq.swiftmodule -emit-library -static -I "$out" "$out/moq.swift" -o target/swift/libMoq.a + swiftc -warnings-as-errors $({{ quote(just_executable()) }} swift-target 13) -swift-version 6 {{ flags }} -parse-as-library -module-name Moq -emit-module -emit-module-path target/swift/Moq.swiftmodule -emit-library -static -I "$out" -I target/swift-collections/build "$out/moq.swift" -o target/swift/libMoq.a + +# Build swift-collections' BasicContainers, which the generated Swift imports, and the modules it imports, at the pinned revision with Swift on PATH, into target/swift-collections/build: a module and a static library each, as its package's manifest for Swift 6.2 builds them. +[private] +swift-collections: + #!/usr/bin/env bash + set -euo pipefail + src=target/swift-collections/src + out=target/swift-collections/build + [ -d "$src/.git" ] || git clone --quiet --filter=blob:none --no-checkout https://github.com/apple/swift-collections "$src" + GIT_NO_LAZY_FETCH=1 git -C "$src" cat-file -e {{ swift_collections_rev }}^{commit} 2> /dev/null || git -C "$src" fetch --quiet origin {{ swift_collections_rev }} + git -C "$src" checkout --quiet --detach {{ swift_collections_rev }} + # Built once for each revision and compiler. + stamp="{{ swift_collections_rev }} $(swiftc --version 2>&1)" + [ "$(cat "$out/stamp" 2> /dev/null)" != "$stamp" ] || exit 0 + rm -rf "$out" + mkdir -p "$out" + flags=(-swift-version 6 -O -parse-as-library -package-name swift_collections -I "$out" $({{ quote(just_executable()) }} swift-target 13) + -enable-upcoming-feature MemberImportVisibility -enable-experimental-feature BuiltinModule + -enable-experimental-feature Lifetimes -enable-experimental-feature InoutLifetimeDependence + -enable-experimental-feature AddressableParameters -enable-experimental-feature AddressableTypes) + while IFS= read -r macro; do + flags+=(-enable-experimental-feature "AvailabilityMacro=$macro") + done < <(sed -nE 's/^ *"(SwiftStdlib [0-9.]+)": *"([^"]+)",?$/\1: \2/p' "$src/Package@swift-6.2.swift") + for module in InternalCollectionsUtilities SpanPreview BasicContainers; do + files=() + while IFS= read -r -d '' file; do files+=("$file"); done < <(find "$src/Sources/$module" -name '*.swift' -print0 | sort -z) + swiftc "${flags[@]}" -module-name "$module" -emit-module -emit-module-path "$out/$module.swiftmodule" -emit-library -static -o "$out/lib$module.a" "${files[@]}" + done + echo "$stamp" > "$out/stamp" # swiftc's target at this macOS deployment target on macOS, and nothing elsewhere. [private] diff --git a/ffi/moq/bench/export.swift b/ffi/moq/bench/export.swift index b1c98de0c..e95a8935e 100644 --- a/ffi/moq/bench/export.swift +++ b/ffi/moq/bench/export.swift @@ -33,13 +33,13 @@ func per(_ start: Span, _ calls: Double = Double(n)) -> Span { return Span(wall: (end.wall - start.wall) / calls, cpu: (end.cpu - start.cpu) / calls) } -/// A value a task sets and this thread reads once the task signals. -final class Cell: @unchecked Sendable { +/// A value a task sets and this thread takes once the task signals. +final class Cell: @unchecked Sendable { var value: T? } /// `body`'s result, waiting on this thread for it to finish on the global executor. -func block(_ body: @escaping @Sendable () async throws -> T) -> T { +func block(_ body: @escaping @Sendable () async throws -> T) -> T { let done = DispatchSemaphore(value: 0) let result = Cell() Task { @@ -47,19 +47,27 @@ func block(_ body: @escaping @Sendable () async throws -> T) -> T { done.signal() } done.wait() - return result.value! + return result.value.take()! +} + +/// The async cases' spans, the last frame read, and their reads' sum, which a tuple cannot +/// hold. +struct Reads: ~Copyable { + var out: [(String, Span)] + var last: FrameFrame + var sum: Int } /// Groups of 100 frames of `size` bytes written and each read back, its payload's first /// byte touched, in about 64 MiB and at least 300 frames: megabytes a wall and a CPU second. -func throughput(_ track: TrackProducer, _ subscriber: TrackSubscriber, _ at: Timestamp, _ size: Int) async throws -> (Span, Int) { +func throughput(_ track: TrackProducer, _ subscriber: inout TrackSubscriber, _ at: Timestamp, _ size: Int) async throws -> (Span, Int) { let count = min(max((64 << 20) / (size * frames), 3), 2000) let frame = Data(count: size) var sum = 0 let start = now() for _ in 0.. ([(String, Span)], Int) { } out.append(("call", per(start))) - let demanded = try track.appendGroup() + var demanded = try track.appendGroup() let demand = demanded.demand() start = now() for _ in 0.. ([(String, Span)], Int) { var micros: UInt64 = 0 start = now() for _ in 0.. ([(String, Span)], Int) { let at = try Timestamp.fromMicros(micros) start = now() for _ in 0.. ([(String, Span)], Int) { } out.append(("write", per(start))) - let (reads, last, readSum) = block { + let reads = block { var out: [(String, Span)] = [] - let subscriber = try await track.consume().subscribe(nil) + var subscriber = try await track.consume().subscribe(nil) // Each frame written, then read through an async func whose future is ready at its // first poll, so it never suspends. var start = now() var last: FrameFrame? for _ in 0.. ([(String, Span)], Int) { // suspend, and the write's wake resumes it on the global executor. start = now() for _ in 0.. ([(String, Span)], Int) { var sum = 0 for size in sizes { - let (mbps, read) = try await throughput(track, subscriber, at, size) + let (mbps, read) = try await throughput(track, &subscriber, at, size) out.append(("mbps:\(size)", mbps)) sum += read } - return (out, last!, sum) + return Reads(out: out, last: last.take()!, sum: sum) } - out += reads - sum += readSum + out += reads.out + sum += reads.sum + let last = reads.last // The last frame's 100 bytes, copied into a Data, and one of them read. start = now() diff --git a/ffi/moq/moq.swift b/ffi/moq/moq.swift index 5220fcf11..532010f9b 100644 --- a/ffi/moq/moq.swift +++ b/ffi/moq/moq.swift @@ -7,6 +7,7 @@ // script drains, after the write that wakes it. A session over QUIC runs on moq-tokio's // fallback runtime, whose threads resume Swift's. +import BasicContainers import Foundation import Moq @@ -46,6 +47,41 @@ final class Cell: @unchecked Sendable { } } +/// A handle a task calls `mutating` methods on: an escaping closure cannot capture a +/// noncopyable `var`, but may a class holding one. +final class Held: @unchecked Sendable { + var value: T + + init(_ value: consuming T) { + self.value = value + } +} + +/// A handle a task takes and the script reads, under a lock: a struct Swift cannot copy, so +/// it moves in and out. +final class Slot: @unchecked Sendable { + private let lock = NSLock() + private var stored: T? = nil + + func put(_ value: consuming T) { + lock.lock() + defer { lock.unlock() } + stored = consume value + } + + func take() -> T? { + lock.lock() + defer { lock.unlock() } + return stored.take() + } + + var isEmpty: Bool { + lock.lock() + defer { lock.unlock() } + return stored == nil + } +} + /// The jobs tasks resume with, run when the script drains them. final class Queue: TaskExecutor, @unchecked Sendable { private let lock = NSLock() @@ -79,31 +115,46 @@ final class Queue: TaskExecutor, @unchecked Sendable { } /// `body`'s result, waiting on this thread for it to finish on the global executor. -func block(_ body: @escaping @Sendable () async throws -> T) throws -> T { +func block(_ body: @escaping @Sendable () async throws -> T) throws -> T { let done = DispatchSemaphore(value: 0) - let result = Cell?>(nil) + let result = Slot() + let failure = Cell(nil) Task { do { - result.value = .success(try await body()) + result.put(try await body()) } catch { - result.value = .failure(error) + failure.value = error } done.signal() } done.wait() - return try result.value!.get() + if let error = failure.value { + throw error + } + return result.take()! } -/// An origin, scoped, published, and polled once by `waiter`, returning its driver and the -/// wakes counted before the origin, along with every object of it, is released on return. -func origin(_ waiter: Waiter, _ wakes: Cell) throws -> (OriginDriver, Int) { +/// Whether `path` is `other`: `==` takes a `Path`, which an optional one holds. +func same(_ path: borrowing Path?, _ other: borrowing Path) -> Bool { + switch path { + case .some(let path): return path == other + case .none: return false + } +} + +/// An origin, scoped, published, and polled once by `waiter`, returning its driver, with +/// `before` the wakes counted before the origin, along with every object of it, is +/// released on return. +func origin(_ waiter: borrowing Waiter, _ wakes: Cell, _ before: inout Int) throws -> OriginDriver { // An origin and the driver that runs it, a pair. Its root, which it lends, is returned // as a new handle to a clone. - let (origin, driver) = OriginProducer.new(OriginConfig.default()) + let pair = OriginProducer.new(OriginConfig.default()) + let origin = pair._0 + var driver = pair._1 expect(origin.root().asStr() == "", "an origin's root is empty") // Scoped beneath a root to patterns, which moq-pattern's own export builds. - let patterns = Patterns.new() + var patterns = Patterns.new() expect(try patterns.insert(Pattern.subtree("alice")), "a pattern inserts") let scoped = try origin.scope("room", patterns) expect(scoped.root().asStr() == "room", "a scope has its root") @@ -131,7 +182,7 @@ func origin(_ waiter: Waiter, _ wakes: Cell) throws -> (OriginDriver, Int) expect(cam.asStr() == "cam", "a path builds from its segments") let joined = try path.join(cam) expect(joined.asStr() == "room/alice/cam", "paths join") - expect(joined.stripPrefix(path) == cam, "a path strips its prefix") + expect(same(joined.stripPrefix(path), cam), "a path strips its prefix") expect(path.stripPrefix(joined) == nil, "a path strips no longer prefix") expect(try joined.resolve(PathRelative.new("../bob")).asStr() == "room/bob", "a relative path resolves") } @@ -140,7 +191,8 @@ func origin(_ waiter: Waiter, _ wakes: Cell) throws -> (OriginDriver, Int) // A client publishes the origin through moq-net's generic `with_publisher`, at each // type that implements its bound, by reference, so Swift keeps its handle. _ = Client.new().withPublisherProducer(origin) - return withExtendedLifetime(origin) { (driver, wakes.value) } + before = wakes.value + return driver } func script() throws { @@ -158,28 +210,36 @@ func script() throws { expect("\(e)" == "duplicate", "a duplicate track's error is Rust's") } - let subscriber = try block { try await track.consume().subscribe(nil) } + let subscriber = Held(try block { try await track.consume().subscribe(nil) }) // A group's arrival wakes the subscriber's pending receive. - let group = Cell(nil) - q.launch { group.value = try await subscriber.recvGroup() } + let group = Slot() + q.launch { + if let received = try await subscriber.value.recvGroup() { + group.put(received) + } + } q.drain() - expect(group.value == nil, "a receive waits for a group") - let writer = try track.appendGroup() + expect(group.isEmpty, "a receive waits for a group") + var writer = try track.appendGroup() expect(q.size == 1, "a group's arrival wakes the receive") q.drain() - let first = group.value! - expect(first.index() == 0, "the first group is 0") + let first = Held(group.take()!) + expect(first.value.index() == 0, "the first group is 0") // So does a frame's. - let frame = Cell(nil) - q.launch { frame.value = try await first.readFrame() } + let frame = Slot() + q.launch { + if let read = try await first.value.readFrame() { + frame.put(read) + } + } q.drain() - expect(frame.value == nil, "a read waits for a frame") + expect(frame.isEmpty, "a read waits for a frame") try writer.writeFrame(nil, Data("hello".utf8)) expect(q.size == 1, "a frame's arrival wakes the read") q.drain() - let hello = frame.value! + let hello = frame.take()! expect(text(hello.payloadGet) == "hello", "a frame has its payload") expect(hello.timestampGet == nil, "an untimed frame has no timestamp") // When it arrived: kio's clock's `Instant`, from kio's own export. @@ -202,8 +262,9 @@ func script() throws { try writer.writeFrame(nil, world) try writer.finish() try block { - expect(try await first.readFrame()?.payloadGet == world, "the last frame reads") - expect(try await first.readFrame() == nil, "a finished group reads nil") + let last = try await first.value.readFrame() + expect(last?.payloadGet == world, "the last frame reads") + expect(try await first.value.readFrame() == nil, "a finished group reads nil") } // The driver, polled as a language's event loop would: at the time, and with a waiter @@ -212,7 +273,8 @@ func script() throws { // of its own ends by returning. let wakes = Cell(0) let waiter = Waiter.new { wakes.value += 1 } - let (driver, before) = try origin(waiter, wakes) + var before = 0 + var driver = try origin(waiter, wakes, &before) expect(wakes.value > before, "the last owner's release wakes the driver") do { _ = try driver.poll(clockNow(), waiter) @@ -247,9 +309,9 @@ func script() throws { // A catalog at moq-mux's untyped extension, from the config Swift builds, whose generic // `is_claimed` is named by the last segment of its argument, and whose `mutate` lends // the catalog to Swift. - let publisher = BroadcastInfo.new().produce() + var publisher = BroadcastInfo.new().produce() let extra = MuxCatalogConfig.default().withCatalogExtra(HangCatalogExtra.default()) - let catalog = try MuxCatalogProducerExtra.new(publisher, extra) + var catalog = try MuxCatalogProducerExtra.new(&publisher, extra) expect(!catalog.isClaimedBinaryConfig("state"), "an unclaimed name is not claimed") let read = Cell(0) try catalog.mutate { lent in @@ -262,7 +324,7 @@ func script() throws { // A JSON track the catalog lists, published through moq-mux's generic `json_snapshot` at // a JSON document, and a section Swift parses from text, set in the lent catalog and // read back as text. - let status = try catalog.jsonSnapshotConfig( + var status = try catalog.jsonSnapshotConfig( try publisher.createTrack("status", TrackInfo.default().withTimescale(Timescale.MILLI)), MuxJsonConfig.default(), MuxJsonOptions.default()) expect(status.name() == "status", "a JSON snapshot has its track's name") expect(catalog.isClaimedJsonConfig("status"), "the JSON snapshot claims its name") @@ -270,12 +332,14 @@ func script() throws { // back as JSON text. let on = try Json.parse(#"{"on": true}"#) try status.update(on, Timestamp.now()) + let latest = Held(status.consume()) try block { - let frame = try await status.consume().recvGroup()?.readFrame() + var group = try await latest.value.recvGroup() + let frame = try await group?.readFrame() expect(text(frame?.payloadGet) == #"{"on":true}"#, "a subscriber reads the latest value") } try catalog.mutate { lent in - let sections = lent.extGet + var sections = lent.extGet try! sections.set("app", Json.parse(#"{"v": [1, true]}"#)) lent.extSet(sections) } @@ -293,7 +357,8 @@ func script() throws { expect(e.error == "EOF while parsing a value at line 1 column 5", "a parse error is serde_json's") } do { - try MuxCatalogHangExtra.default().set("json", Json.parse("1")) + var hang = MuxCatalogHangExtra.default() + try hang.set("json", Json.parse("1")) expect(false, "a reserved section is an error") } catch let e as MuxError { expect("\(e)" == "reserved catalog section: json", "a reserved section is moq-mux's error") @@ -311,17 +376,17 @@ func script() throws { expect(e.error == "relative URL without a base", "a URL's parse error is Rust's") } // A session's config, its URL and pinned fingerprints set as Rust assigns them. - let connect = TokioConnectConfig.default() - let tls = connect.tlsGet + var connect = TokioConnectConfig.default() + var tls = connect.tlsGet tls.fingerprintSet(["ab:cd"]) connect.tlsSet(tls) connect.urlSet(try Url.parse("https://relay.example:4443/demo")) expect(connect.tlsGet.fingerprintGet == ["ab:cd"], "a TLS config keeps its fingerprints") let url = connect.urlGet! let versions = Versions.default() - let dialed = AuthTokens.new([]).dial(url, versions).urlGet + let dialed = AuthTokens.new(UniqueArray()).dial(url, versions).urlGet expect(dialed == url, "a dial keeps its URL") - expect("\(dialed)" == "https://relay.example:4443/demo", "a URL prints as Rust's") + expect(dialed.description == "https://relay.example:4443/demo", "a URL prints as Rust's") // A `Vec`, as an array. expect(!ALPNS.isEmpty, "a constant slice crosses as an array") let alpns = versions.alpns() @@ -332,7 +397,7 @@ func script() throws { expect("\(bind)" == "[::1]:4443", "an IPv6 bind prints as Rust's") let v4 = SocketAddr(ip: [127, 0, 0, 1], port: 0) expect("\(TokioListenBind.addr(v4))" == "127.0.0.1:0", "an IPv4 bind prints as Rust's") - let listen = TokioListenConfig.default() + var listen = TokioListenConfig.default() listen.bindSet(bind) // The preferred address of each family, which takes only that family. listen.preferredV4Set(v4) @@ -355,28 +420,31 @@ func script() throws { let live = try served.createBroadcast("live") try live.announce(OriginRoute.default()) let liveTrack = try live.broadcast().createTrack("video", nil) - let liveGroup = try liveTrack.appendGroup() + var liveGroup = try liveTrack.appendGroup() try liveGroup.writeFrame(nil, Data("hello".utf8)) try liveGroup.finish() - let serverConfig = TokioServerConfig.default() - let listen = serverConfig.listenGet + var serverConfig = TokioServerConfig.default() + var listen = serverConfig.listenGet listen.bindSet(.addr(SocketAddr(ip: Array(repeating: 0, count: 16), port: 0))) - let tls = listen.tlsGet + var tls = listen.tlsGet tls.generateSet(["localhost"]) listen.tlsSet(tls) serverConfig.listenSet(listen) let server = try serverConfig.init_() let local = try server.localAddr() expect(local.port != 0, "a server bound to port 0 reads back the port it got") - let serving = Task { try await server.servePublish(served.consume()) } + // Serving consumes the server, which the task takes. + let owned = Slot() + owned.put(server) + let serving = Task { try await owned.take()!.servePublish(served.consume()) } - let connect = TokioConnectConfig.default() + var connect = TokioConnectConfig.default() connect.urlSet(try Url.parse("moqt://localhost:\(local.port)")) - let insecure = connect.tlsGet + var insecure = connect.tlsGet insecure.insecureSet(true) connect.tlsSet(insecure) - let clientConfig = TokioClientConfig.default() + var clientConfig = TokioClientConfig.default() clientConfig.connectSet(connect) let client = try clientConfig.init_() let received = tokioOriginSpawn() @@ -385,8 +453,9 @@ func script() throws { expect(connection != nil, "a client with a URL dials it") let established = try await connection!.established() - let subscriber = try await remote.request("live", nil).track("video").subscribe(nil) - let frame = try await subscriber.recvGroup()?.readFrame() + var subscriber = try await remote.request("live", nil).track("video").subscribe(nil) + var group = try await subscriber.recvGroup() + let frame = try await group?.readFrame() expect(text(frame?.payloadGet) == "hello", "a session reads the frame the server serves") try await established.close() diff --git a/ffi/runtime/rt.swift b/ffi/runtime/rt.swift index a672925ca..414598db2 100644 --- a/ffi/runtime/rt.swift +++ b/ffi/runtime/rt.swift @@ -1,6 +1,7 @@ // The runtime every generated Swift file includes, after `Native` names the export's C -// types and functions: the handles objects own, the arena a call lowers its values into, -// bytes and text across the C ABI, and the signal a future parks on. +// types and functions: the handles classes own, the arena a call lowers its values into, +// bytes and text across the C ABI, and the signal a future parks on. Each helper that +// returns what its body does takes a noncopyable result, an object's struct. /// An IP socket address, as Rust's `SocketAddr` holds it. public struct SocketAddr: Hashable, Sendable { @@ -76,32 +77,37 @@ extension Native { static var none: Buf { Buf(ptr: nil, len: 0, owner: nil) } - /// `bytes` as a view C reads during `body`, without a copy. + /// `bytes` as a view C reads during `body`, without a copy. Foundation's closure + /// returns only a copyable result, so `body`'s leaves through an optional. @inline(__always) - static func view(_ bytes: Data, _ body: (Buf) throws -> R) rethrows -> R { + static func view(_ bytes: Data, _ body: (Buf) throws -> R) rethrows -> R { + var result: R? = nil try bytes.withUnsafeBytes { raw in let ptr = raw.baseAddress?.assumingMemoryBound(to: UInt8.self) ?? UnsafePointer(empty) - return try body(Buf(ptr: ptr, len: raw.count, owner: nil)) + result = try body(Buf(ptr: ptr, len: raw.count, owner: nil)) } + return result.take()! } @inline(__always) - static func view(_ bytes: Data?, _ body: (Buf) throws -> R) rethrows -> R { + static func view(_ bytes: Data?, _ body: (Buf) throws -> R) rethrows -> R { guard let bytes else { return try body(none) } return try view(bytes, body) } /// `text`'s UTF-8 as a view C reads during `body`, without a copy for a native string. @inline(__always) - static func view(_ text: Swift.String, _ body: (Buf) throws -> R) rethrows -> R { + static func view(_ text: Swift.String, _ body: (Buf) throws -> R) rethrows -> R { var text = text - return try text.withUTF8 { utf8 in - try body(Buf(ptr: utf8.baseAddress ?? UnsafePointer(empty), len: utf8.count, owner: nil)) + var result: R? = nil + try text.withUTF8 { utf8 in + result = try body(Buf(ptr: utf8.baseAddress ?? UnsafePointer(empty), len: utf8.count, owner: nil)) } + return result.take()! } @inline(__always) - static func view(_ text: Swift.String?, _ body: (Buf) throws -> R) rethrows -> R { + static func view(_ text: Swift.String?, _ body: (Buf) throws -> R) rethrows -> R { guard let text else { return try body(none) } return try view(text, body) } @@ -251,9 +257,31 @@ extension Native { return try UnsafeBufferPointer(start: ptr, count: len).map(lift) } - /// A handle an object owns, or is lent for a call: each call holds it, so `close`, - /// once a call moves its value out or a lend ends, frees it only once no call does, and - /// a call after `close` traps. + /// As `list`, of items Swift cannot copy. + static func unique( + _ ptr: UnsafePointer?, _ len: Int, _ lift: (C) throws -> T + ) rethrows -> UniqueArray { + var items = UniqueArray(minimumCapacity: len) + for i in 0..(_ value: borrowing T?, _ some: (borrowing T) throws -> C, _ none: C) rethrows -> C { + switch value { + case .some(let value): return try some(value) + case .none: return none + } + } + + /// A handle a class owns, or is lent for a call: each call holds it, so `close`, once + /// a call moves its value out or a lend ends, frees it only once no call does, and a + /// call after `close` traps. A struct needs none of this: Swift's compiler orders every + /// call before its move or the end of its lend. struct Handle { let raw: OpaquePointer? /// The holds, one for the owner until closed and one per call, shifted past a bit @@ -296,7 +324,7 @@ extension Native { /// Run `body` with the raw handle, held until it returns. @inline(__always) - func with(_ body: (OpaquePointer?) throws -> R) rethrows -> R { + func with(_ body: (OpaquePointer?) throws -> R) rethrows -> R { let raw = hold() defer { release() } return try body(raw) @@ -335,7 +363,7 @@ extension Native { /// Run `body` with the raw handle of `handle`, or null for none, held until it returns. @inline(__always) - static func with(_ handle: Handle?, _ body: (OpaquePointer?) throws -> R) rethrows -> R { + static func with(_ handle: Handle?, _ body: (OpaquePointer?) throws -> R) rethrows -> R { guard let handle else { return try body(nil) } return try handle.with(body) } @@ -394,6 +422,18 @@ extension Native { return (UnsafePointer(array), items.count) } + /// As `list`, of items Swift cannot copy, each borrowed. + func unique( + _ items: borrowing UniqueArray, _ lower: (borrowing T) throws -> C + ) rethrows -> (UnsafePointer, Int) { + let array = UnsafeMutablePointer.allocate(capacity: Swift.max(items.count, 1)) + blocks.append(UnsafeMutableRawPointer(array)) + for i in 0..( + static func run( _ raw: OpaquePointer?, _ free: @convention(c) (OpaquePointer?) -> Void, _ poll: (OpaquePointer?, CWaker) throws -> T? ) async throws -> T { @@ -536,12 +576,15 @@ extension Native { } let signal = Signal() var given = false - return try await withTaskCancellationHandler { + // The handler's operation returns only a copyable result, so it leaves through this. + var output: T? = nil + try await withTaskCancellationHandler { while true { signal.reset() try Task.checkCancellation() - if let output = try poll(raw, signal.waker(given: !given)) { - return output + if let ready = try poll(raw, signal.waker(given: !given)) { + output = consume ready + return } given = true await signal.park() @@ -549,5 +592,6 @@ extension Native { } onCancel: { signal.wake() } + return output.take()! } } diff --git a/ffi/src/driver.rs b/ffi/src/driver.rs index 04334df13..b49a6abab 100644 --- a/ffi/src/driver.rs +++ b/ffi/src/driver.rs @@ -262,6 +262,12 @@ impl Export { .with_span_note(*first_span, format!("{first} is here")) .emit(); } + for (span, why) in crate::swift::refusals(&abi) { + tcx.dcx().span_err( + span, + format!("rust2-ffi: {why}; exclude it in the config to leave it out"), + ); + } if tcx.dcx().has_errors().is_some() { return Ok(()); } diff --git a/ffi/src/swift.rs b/ffi/src/swift.rs index 3c33af187..ec1267d70 100644 --- a/ffi/src/swift.rs +++ b/ffi/src/swift.rs @@ -4,12 +4,17 @@ //! Each name is its C name without the prefix, as Swift spells it: `moq_track_producer` //! is `TrackProducer`, and its method `moq_track_producer_append_group` is //! `TrackProducer.appendGroup`. A keyword is quoted in backticks, and a member named as one -//! Swift gives its type takes a trailing `_`. An object is a final class owning its handle, -//! freed in `deinit`; a record a struct; an enum a Swift enum, with +//! Swift gives its type takes a trailing `_`. An object is a noncopyable struct owning its +//! handle, freed in `deinit`, so Swift's compiler checks each move and borrow Rust makes: +//! `self` is `consuming`, `&mut self` `mutating`, a parameter `consuming`, `borrowing`, or +//! `inout`, a tuple holding one a struct of its own, and an array of them swift-collections' +//! `UniqueArray`. An object that is `Copy` or thrown as an error is a final class instead, +//! since `Swift.Error` requires a copyable type, whose handle a call holds at run time. A +//! record is a struct; an enum a Swift enum, with //! its data as associated values; a trait a protocol; a closure a function type; a future //! an `async` function; an error a thrown `Swift.Error`. The runtime they share, -//! [`runtime/rt.swift`](../runtime/rt.swift), holds the handles, the arena a call lowers -//! nested values into, and the signal a future parks on. +//! [`runtime/rt.swift`](../runtime/rt.swift), holds the classes' handles, the arena a call +//! lowers nested values into, and the signal a future parks on. //! //! A call passes bytes and text at its top level as views of Swift's own memory, and //! returned bytes are `Data` lending Rust's buffer, so neither is copied; nested in another @@ -18,8 +23,10 @@ use std::collections::{BTreeMap, BTreeSet, HashMap}; use std::fmt::Write; +use rustc_span::Span; + use crate::abi::{self, Abi, Ty, pascal}; -use crate::ir::{Access, Function, Int, Literal, On, Param, Receiver, Type}; +use crate::ir::{Access, Function, Int, Literal, Mutability, On, Param, Receiver, Type}; const RUNTIME: &str = include_str!("../runtime/rt.swift"); @@ -49,6 +56,7 @@ pub fn generate(abi: &Abi<'_>, packages: &str, contract: u64) -> Result, packages: &str, contract: u64) -> Result { thrown: BTreeMap, /// The exported types thrown as errors, which conform to `Swift.Error`. conforms: BTreeSet, + /// The objects that are noncopyable structs: each but those `Copy` or thrown. + structs: BTreeSet, fresh: usize, /// The targets of the item being written, whose calls check theirs. on: On, @@ -248,6 +259,44 @@ impl<'a> Gen<'a> { } } + /// Whether Swift's value of `ty` is noncopyable: it holds an object's struct. + fn unique(&self, ty: &Type) -> bool { + unique(&self.structs, ty) + } + + /// The ownership a parameter of type `ty` declares, which Swift requires of a + /// noncopyable one: `&T` borrows it, `&mut T` is `inout`, and a value moves in. + fn ownership(&self, ty: &Type) -> &'static str { + match ty { + _ if !self.unique(ty) => "", + Type::Ref(Mutability::Not, _) => "borrowing ", + Type::Ref(Mutability::Mut, _) => "inout ", + Type::Option(inner) if matches!(**inner, Type::Ref(Mutability::Mut, _)) => "inout ", + _ => "consuming ", + } + } + + /// The Swift value of the `n`th element of the tuple `e` of `elements`: a struct's + /// field `_n` if it is noncopyable, else the tuple's element. + fn element(&self, elements: &[Type], e: &str, n: usize) -> String { + match elements.iter().any(|e| self.unique(e)) { + true => format!("{e}._{n}"), + false => format!("{e}.{n}"), + } + } + + /// Whether passing `ty` moves the value of a class's handle out, so the call closes it: + /// by value, boxed, or in an `Option`, a list, or a tuple, unless `Copy`, and never + /// through a reference. A struct needs no close: Swift's compiler ends it. + fn moved(&self, ty: &Type) -> bool { + match ty { + Type::Object(i) => !self.abi.api.objects[*i].copy && !self.structs.contains(i), + Type::Option(inner) | Type::Box(inner) | Type::List(_, inner) => self.moved(inner), + Type::Tuple(elements) => elements.iter().any(|ty| self.moved(ty)), + _ => false, + } + } + /// The Swift type of `ty`, as a value, a parameter, or a result. fn sw(&self, ty: &Type) -> String { let abi = self.abi; @@ -265,6 +314,7 @@ impl<'a> Gen<'a> { Type::U128 => "U128".to_string(), Type::Duration => "Swift.Duration".to_string(), Type::SocketAddr(_) => "SocketAddr".to_string(), + Type::List(_, inner) if self.unique(inner) => format!("UniqueArray<{}>", self.sw(inner)), Type::List(_, inner) => format!("[{}]", self.sw(inner)), Type::Waker => "@Sendable () -> Void".to_string(), Type::Option(inner) => format!("{}?", self.sw(inner)), @@ -274,13 +324,16 @@ impl<'a> Gen<'a> { Type::Object(i) => self.class(&abi.object(*i)), Type::Callback(i, _) => format!("any {}", self.class(&abi.callback(*i))), Type::Closure(params, ret) => { - let params: Vec = params.iter().map(|p| self.sw(p)).collect(); + let params: Vec = (params.iter()) + .map(|p| format!("{}{}", self.ownership(p), self.sw(p))) + .collect(); format!("@Sendable ({}) -> {}", params.join(", "), self.sw(ret)) } Type::Range(_) | Type::Bound(_) | Type::Bounds(_) => self.class(&abi.range(ty)), Type::Tuple(elements) => match &elements[..] { // Swift has no tuple of one: it is its element, which loses nothing. [element] => self.sw(element), + elements if self.unique(ty) => self.class(&abi.tuple(elements)), elements => { let elements: Vec = elements.iter().map(|e| self.sw(e)).collect(); format!("({})", elements.join(", ")) @@ -344,7 +397,8 @@ impl<'a> Gen<'a> { } /// The C value of the Swift value `e` of type `ty`, nested in a value the call passes: - /// bytes copied into the arena `a_`, and handles held by it. + /// bytes copied into the arena `a_`, and a class's handle held by it. A struct's handle + /// needs no hold, since the call borrows or consumes the struct until it returns. fn lower(&mut self, ty: &Type, e: &str) -> String { let abi = self.abi; if Abi::is_buf(ty) { @@ -364,11 +418,31 @@ impl<'a> Gen<'a> { Type::List(_, inner) => { let x = self.fresh("x"); let item = self.lower(inner, &x); + let list = match self.unique(inner) { + true => "unique", + false => "list", + }; format!( - "Native.vec(a_.list({e}) {{ {x} in {item} }}, {}.init(ptr:len:owner:))", + "Native.vec(a_.{list}({e}) {{ {x} in {item} }}, {}.init(ptr:len:owner:))", abi.list(inner) ) } + // `Optional.map` takes only a copyable value: `Native.option` borrows it. + Type::Option(inner) if self.unique(inner) => { + let v = self.fresh("v"); + let (some, none) = match inner.base() { + Type::Object(_) => (format!("{v}.handle"), "nil".to_string()), + Type::List(_, elem) => (self.lower(inner, &v), format!("{}()", abi.list(elem))), + _ => { + let c = abi.option(inner); + ( + format!("{c}(some: true, value: {})", self.lower(inner, &v)), + format!("{c}()"), + ) + } + }; + format!("Native.option({e}, {{ {v} in {some} }}, {none})") + } Type::Option(inner) => match inner.base() { Type::Object(_) => format!("a_.hold({e}?.handle)"), _ if Abi::is_buf(inner) => format!("a_.buf({e})"), @@ -386,6 +460,7 @@ impl<'a> Gen<'a> { }, Type::Box(inner) | Type::Ref(_, inner) => self.lower(inner, e), Type::Record(_) | Type::Enum(_) => format!("{e}._lower({})", self.arena_arg(ty)), + Type::Object(_) if self.unique(ty) => format!("{e}.handle"), Type::Object(_) => format!("a_.hold({e}.handle)"), Type::Callback(i, _) => format!("Native.lower_{}({e})", abi.callback(*i)), Type::Tuple(elements) => { @@ -393,7 +468,7 @@ impl<'a> Gen<'a> { let fields: Vec = match &elements[..] { [element] => vec![format!("_0: {}", self.lower(element, e))], elements => (elements.iter().enumerate()) - .map(|(n, ty)| format!("_{n}: {}", self.lower(ty, &format!("{e}.{n}")))) + .map(|(n, ty)| format!("_{n}: {}", self.lower(ty, &self.element(elements, e, n)))) .collect(), }; format!("{c}({})", fields.join(", ")) @@ -422,8 +497,12 @@ impl<'a> Gen<'a> { Type::List(_, inner) => { let (v, c) = (self.fresh("v"), self.fresh("c")); let item = self.lift(inner, &c); + let list = match self.unique(inner) { + true => "unique", + false => "list", + }; format!( - "{{ () -> {} in var {v} = {e}; defer {{ {v}.len = 0; {}_free({v}) }}; return Native.list({v}.ptr, {v}.len) {{ {c} in {item} }} }}()", + "{{ () -> {} in var {v} = {e}; defer {{ {v}.len = 0; {}_free({v}) }}; return Native.{list}({v}.ptr, {v}.len) {{ {c} in {item} }} }}()", self.sw(ty), abi.list(inner) ) @@ -454,7 +533,10 @@ impl<'a> Gen<'a> { let lifted: Vec = (elements.iter().enumerate()) .map(|(n, ty)| self.lift(ty, &format!("{e}._{n}"))) .collect(); - format!("({})", lifted.join(", ")) + match self.unique(ty) { + true => format!("{}({})", self.sw(ty), lifted.join(", ")), + false => format!("({})", lifted.join(", ")), + } } }, other => unreachable!("{other:?} is never returned"), @@ -485,6 +567,35 @@ impl<'a> Gen<'a> { for (c, ty) in &abi.ranges.clone() { self.range(c, ty); } + // Every tuple holding a struct, wherever it is, a whole return's included, which C + // returns through an out pointer per element rather than a struct. + let methods = (api.records.iter().flat_map(|r| &r.methods)) + .chain(api.enums.iter().flat_map(|e| &e.methods)) + .chain(api.objects.iter().flat_map(|o| &o.methods)); + let mut tuples = BTreeMap::new(); + for function in api.functions.iter().chain(methods) { + let types = (function.params.iter().map(|p| &p.ty)) + .chain([&function.ok]) + .chain(&function.err); + for ty in types { + crate::ir::each(&strip_refs(ty), &mut |ty| { + if let Type::Tuple(elements) = ty + && elements.len() > 1 + && self.unique(ty) + { + tuples.insert(abi.tuple(elements), elements.clone()); + } + }); + } + } + // Each named before any is written, since one tuple's fields may name another. + for c in tuples.keys() { + let name = self.claim(&self.class(c), &[], format!("the tuple `{c}`")); + self.classes.insert(c.clone(), name); + } + for (c, elements) in &tuples { + self.tuple(c, elements); + } for (c, n) in &abi.arrays { // Its bytes inline, in C's struct, as `InlineArray` would hold them from macOS 26. let name = Abi::array_class(*n); @@ -519,7 +630,7 @@ impl<'a> Gen<'a> { self.callback(i); } for function in &api.functions { - if let Some((code, _)) = self.function(function, None, None, &[]) { + for (code, _) in self.function(function, None, None, &[]) { self.top.push_str(&code); self.top.push('\n'); } @@ -537,6 +648,27 @@ impl<'a> Gen<'a> { } } + /// The struct of a tuple holding an object's struct, which a Swift tuple cannot hold: + /// its fields `_0`, `_1`, ... are the tuple's elements. + fn tuple(&mut self, c: &str, elements: &[Type]) { + let name = self.class(c); + let types: Vec = elements.iter().map(|e| self.sw(e)).collect(); + let fields: String = (types.iter().enumerate()) + .map(|(n, sw)| format!("\tpublic var _{n}: {sw}\n")) + .collect(); + let params: Vec = (elements.iter().zip(&types).enumerate()) + .map(|(n, (e, sw))| format!("_ _{n}: {}{sw}", self.ownership(e))) + .collect(); + let assigns: String = (0..elements.len()).map(|n| format!("\t\tself._{n} = _{n}\n")).collect(); + let shown: Vec<&str> = types.iter().map(|t| t.trim_start_matches("Swift.")).collect(); + let _ = write!( + self.types, + "/// The tuple `({})`, as a struct, since it holds a value Swift cannot copy. Frozen, so\n/// another module may move each element out.\n@frozen public struct {name}: ~Copyable, Sendable {{\n{fields}\n\tpublic init({}) {{\n{assigns}\t}}\n}}\n\n", + shown.join(", "), + params.join(", ") + ); + } + /// A struct for a range or pair of bounds, or an enum for a bound, and its conversions. fn range(&mut self, c: &str, ty: &Type) { let name = self.class(c); @@ -590,8 +722,10 @@ impl<'a> Gen<'a> { ) { let name = self.sw(ty); let object = matches!(ty, Type::Object(_)); - // The receiver: a held handle, or the value lowered. + let unique = self.unique(ty); + // The receiver: a struct's handle, a class's held, or the value lowered. let (open, this, close) = match object { + true if unique => (String::new(), "self.handle", ""), true => ("self.handle.with { h_ in ".to_string(), "h_", " }"), false => { let arena = match self.arena(ty) { @@ -631,13 +765,18 @@ impl<'a> Gen<'a> { } if object && t.clone { members.push("clone".to_string()); + let raw = match unique { + true => format!("{c}_clone(self.handle)"), + false => format!("self.handle.with {{ {c}_clone($0) }}"), + }; body.push(format!( - "\t/// A new handle to a clone.\n\tpublic func clone() -> {name} {{\n\t\t{name}(raw: self.handle.with {{ {c}_clone($0) }})\n\t}}\n" + "\t/// A new handle to a clone.\n\tpublic func clone() -> {name} {{\n\t\t{name}(raw: {raw})\n\t}}\n" )); } if t.eq { protocols.push("Equatable"); let compare = match object { + true if unique => format!("{c}_eq(a.handle, b.handle)"), true => format!("a.handle.with {{ a_ in b.handle.with {{ b_ in {c}_eq(a_, b_) }} }}"), false => { let arena = match self.arena(ty) { @@ -651,8 +790,12 @@ impl<'a> Gen<'a> { ) } }; + let borrowing = match unique { + true => "borrowing ", + false => "", + }; body.push(format!( - "\tpublic static func == (a: {name}, b: {name}) -> Swift.Bool {{\n\t\t{compare}\n\t}}\n" + "\tpublic static func == (a: {borrowing}{name}, b: {borrowing}{name}) -> Swift.Bool {{\n\t\t{compare}\n\t}}\n" )); } if t.default { @@ -678,7 +821,7 @@ impl<'a> Gen<'a> { members: &mut Vec, ) { for method in methods { - if let Some((code, member)) = self.function(method, Some(owner), Some(ty), members) { + for (code, member) in self.function(method, Some(owner), Some(ty), members) { members.push(member); body.push(indent(&code)); } @@ -867,10 +1010,17 @@ impl<'a> Gen<'a> { let name = self.class(&c); let rust = abi.api.rust(&object.path, &object.args); let ty = Type::Object(i); + let unique = self.unique(&ty); let mut members = vec!["handle".to_string()]; - let mut body = vec![format!( - "\tlet handle: Native.Handle\n\n\tinit(raw: OpaquePointer?) {{\n\t\thandle = Native.Handle(raw, {c}_free, \"{name}\")\n\t}}\n\n\tinit(lent raw: OpaquePointer?) {{\n\t\thandle = Native.Handle(raw, nil, \"{name}\")\n\t}}\n\n\tdeinit {{\n\t\thandle.drop()\n\t}}\n" - )]; + let mut body = vec![match unique { + // A lend ends with `_end`, which leaves Rust the handle it lent to free. + true => format!( + "\tlet handle: OpaquePointer?\n\n\tinit(raw: OpaquePointer?) {{\n\t\thandle = raw\n\t}}\n\n\tdeinit {{\n\t\t{c}_free(handle)\n\t}}\n\n\tconsuming func _end() {{\n\t\tdiscard self\n\t}}\n" + ), + false => format!( + "\tlet handle: Native.Handle\n\n\tinit(raw: OpaquePointer?) {{\n\t\thandle = Native.Handle(raw, {c}_free, \"{name}\")\n\t}}\n\n\tinit(lent raw: OpaquePointer?) {{\n\t\thandle = Native.Handle(raw, nil, \"{name}\")\n\t}}\n\n\tdeinit {{\n\t\thandle.drop()\n\t}}\n" + ), + }]; let mut protocols = vec!["@unchecked Sendable"]; self.traits(&ty, &c, object.traits, &mut body, &mut members, &mut protocols); let owner = Ty::Handle { @@ -880,13 +1030,28 @@ impl<'a> Gen<'a> { self.methods(&object.methods, &owner, &ty, &mut body, &mut members); self.distinct(&rust, &members); let text = format!("{rust}\n\n{}", object.docs); + let docs = docs(text.trim(), ""); + let body = body.join("\n"); + if !unique { + let _ = write!( + self.types, + "{docs}public final class {name}: {} {{\n{body}}}\n\n", + protocols.join(", ") + ); + return; + } let _ = write!( self.types, - "{}public final class {name}: {} {{\n{}}}\n\n", - docs(text.trim(), ""), - protocols.join(", "), - body.join("\n") + "{docs}public struct {name}: ~Copyable, @unchecked Sendable {{\n{body}}}\n\n" ); + // Swift 6.4 takes a noncopyable type's `Equatable`, but not yet its + // `CustomStringConvertible` or `CustomDebugStringConvertible`, whose members it keeps. + if protocols.contains(&"Equatable") { + let _ = write!( + self.types, + "#if compiler(>=6.4)\nextension {name}: Equatable {{}}\n#endif\n\n" + ); + } } fn callback(&mut self, i: usize) { @@ -953,6 +1118,17 @@ impl<'a> Gen<'a> { /// call moved them out. fn close(&mut self, ty: &Type, e: &str) -> String { match ty { + // A struct's container is borrowed, since `if let` and `for in` would consume it. + Type::Option(inner) if self.unique(inner) => { + let v = self.fresh("v"); + let close = self.close(inner, &v); + format!("Native.option({e}, {{ {v} in {close} }}, ())") + } + Type::List(_, inner) if self.unique(inner) => { + let i = self.fresh("i"); + let close = self.close(inner, &format!("{e}[{i}]")); + format!("for {i} in {e}.indices {{ {close} }}") + } Type::Option(inner) => { let v = self.fresh("v"); let close = self.close(inner, &v); @@ -961,10 +1137,13 @@ impl<'a> Gen<'a> { Type::Box(inner) => self.close(inner, e), Type::Tuple(elements) if elements.len() == 1 => self.close(&elements[0], e), Type::Tuple(elements) => { - let closes: Vec = (elements.iter().enumerate()) - .filter(|(_, ty)| moved(self.abi, ty)) - .map(|(k, ty)| self.close(ty, &format!("{e}.{k}"))) - .collect(); + let mut closes = Vec::new(); + for (k, ty) in elements.iter().enumerate() { + if self.moved(ty) { + let element = self.element(elements, e, k); + closes.push(self.close(ty, &element)); + } + } closes.join("; ") } Type::List(_, inner) => { @@ -982,7 +1161,7 @@ impl<'a> Gen<'a> { let abi = self.abi; let mut arg = Arg::default(); let mut arena = false; - if moved(abi, ty) { + if self.moved(ty) { arg.after.push(self.close(ty, e)); } match (ty, c) { @@ -1004,6 +1183,11 @@ impl<'a> Gen<'a> { arg.open = Some((format!("{tr}Native.view({e}) {{ {b} in"), "}".to_string())); arg.exprs.push(b); } + // A struct the call borrows or consumes, so it lives until the call returns. + (ty, Ty::Handle { .. }) if self.unique(ty) => arg.exprs.push(match ty.base() { + Type::Option(_) => format!("Native.option({e}, {{ $0.handle }}, nil)"), + _ => format!("{e}.handle"), + }), (ty, Ty::Handle { .. }) => { let h = format!("h{k}_"); let open = match ty.base() { @@ -1022,7 +1206,9 @@ impl<'a> Gen<'a> { } /// The C function C calls for a closure of type `ty`, lent for a call, taking `lent` - /// and returning `ret`: it lends each object as its class, ended once it returns. + /// and returning `ret`: it lends each object as its type, ended once it returns. A + /// struct lent `&mut` is `inout`, which Swift may assign another value; Rust's stays + /// its own, so that traps. fn trampoline(&mut self, ty: &Type, lent: &[Type], ret: &Type) -> String { let ps: Vec = (0..lent.len()).map(|k| format!("p{k}_")).collect(); let mut body = vec![format!( @@ -1030,8 +1216,27 @@ impl<'a> Gen<'a> { self.sw(ty) )]; let mut args = Vec::new(); + let mut ends = Vec::new(); for (k, (ty, p)) in lent.iter().zip(&ps).enumerate() { match ty { + Type::Ref(m, object) if self.unique(object) => { + let l = format!("l{k}_"); + let name = self.sw(object); + match m { + Mutability::Not => { + body.push(format!("let {l} = {name}(raw: {p})")); + args.push(l.clone()); + } + Mutability::Mut => { + body.push(format!("var {l} = {name}(raw: {p})")); + args.push(format!("&{l}")); + ends.push(format!( + "if {l}.handle != {p} {{ fatalError(\"{name}: a lent handle was replaced\") }}" + )); + } + } + ends.push(format!("{l}._end()")); + } Type::Ref(_, object) if matches!(**object, Type::Object(_)) => { let l = format!("l{k}_"); body.push(format!("let {l} = {}(lent: {p})", self.sw(object))); @@ -1042,24 +1247,68 @@ impl<'a> Gen<'a> { } } let call = format!("f_({})", args.join(", ")); - body.push(match ret { - Type::Unit => call, - ret => format!("return {}", self.lower(ret, &call)), - }); + match (ret, ends.is_empty()) { + (Type::Unit, _) => { + body.push(call); + body.extend(ends); + } + (ret, true) => body.push(format!("return {}", self.lower(ret, &call))), + (ret, false) => { + body.push(format!("let r_ = {call}")); + body.extend(ends); + body.push(format!("return {}", self.lower(ret, "r_"))); + } + } let mut params = vec!["data_".to_string()]; params.extend(ps); format!("{{ {} in\n\t{}\n}}", params.join(", "), body.join("\n\t")) } - /// `function`'s Swift form and its member name, a member of `owner` (whose IR type is - /// `owner_ty`) if it has one, apart from the members `taken` names. + /// `function`'s Swift forms and their member names, members of `owner` (whose IR type + /// is `owner_ty`) if it has one, apart from the members `taken` names: an overload for + /// each choice of passing or leaving out each `Option<&T>` of a struct, since an optional + /// holds a struct only by consuming it. fn function( &mut self, function: &Function, owner: Option<&Ty>, owner_ty: Option<&Type>, taken: &[String], - ) -> Option<(String, String)> { + ) -> Vec<(String, String)> { + let optional: Vec = (function.params.iter().enumerate()) + .filter(|(_, p)| self.lent_option(&p.ty)) + .map(|(k, _)| k) + .collect(); + assert!( + optional.len() <= MAX_OPTIONAL, + "the driver refuses past the bound first" + ); + (0..1usize << optional.len()) + .map(|mask| { + let left: BTreeSet = (optional.iter().enumerate()) + .filter(|(bit, _)| mask & (1 << bit) == 0) + .map(|(_, k)| *k) + .collect(); + self.overload(function, owner, owner_ty, taken, &left) + }) + .collect() + } + + /// Whether `ty` is an `Option<&T>` of a struct. + fn lent_option(&self, ty: &Type) -> bool { + lent_option(&self.structs, ty) + } + + /// One of `function`'s Swift forms, apart from the members `taken` names, leaving out + /// the optional borrowed parameters in `left`, which pass `None`. + fn overload( + &mut self, + function: &Function, + owner: Option<&Ty>, + owner_ty: Option<&Type>, + taken: &[String], + left: &BTreeSet, + ) -> (String, String) { let abi = self.abi; let export = abi.export(function, owner); let c_name = export.call.name.clone(); @@ -1083,6 +1332,14 @@ impl<'a> Gen<'a> { let receiver = function.receiver.filter(|_| owner.is_some()); let object = matches!(owner, Some(Ty::Handle { .. })) && receiver.is_some(); + // A struct's method takes `self` as Rust does: by value, it consumes it, and Swift + // then frees the handle Rust moved the value out of. + let unique = owner_ty.is_some_and(|ty| self.unique(ty)); + let mutation = match receiver { + Some(Receiver::Owned) if unique => "consuming ", + Some(Receiver::Mut) if unique => "mutating ", + _ => "", + }; // Only a future or a `Result` throws, and a future only once it is awaited. let throws = function.future || function.err.is_some(); let tr = match function.err.is_some() && !function.future { @@ -1104,6 +1361,7 @@ impl<'a> Gen<'a> { let ty = owner_ty.expect("a method has its type").clone(); let mut arg = Arg::default(); match c { + Ty::Handle { .. } if unique => arg.exprs.push("self.handle".to_string()), Ty::Handle { .. } => { arg.open = Some((format!("{tr}self.handle.with {{ h_ in"), "}".to_string())); arg.exprs.push("h_".to_string()); @@ -1127,13 +1385,33 @@ impl<'a> Gen<'a> { if let Type::Closure(..) = param.ty { c_params.next().expect("a closure has its data"); } - let inout = match (param.ty.written(), ¶m.ty) { + if left.contains(&k) { + args.push(Arg { + exprs: vec!["nil".to_string()], + ..Arg::default() + }); + continue; + } + if let Type::Option(lent) = ¶m.ty + && self.lent_option(¶m.ty) + { + // Labelled, as a defaulted argument is, so that overloads leaving out + // different ones differ in name. + params.push(format!("{name}: borrowing {}", self.sw(lent))); + args.push(Arg { + exprs: vec![format!("{name}.handle")], + ..Arg::default() + }); + continue; + } + // A value Rust writes back is `inout`, as is an object it borrows mutably. + let ownership = match (param.ty.written(), ¶m.ty) { (true, _) => "inout ", // Rust keeps the waker past the call. (false, Type::Waker) => "@escaping ", - (false, _) => "", + (false, _) => self.ownership(¶m.ty), }; - params.push(format!("_ {name}: {inout}{}", self.sw(¶m.ty))); + params.push(format!("_ {name}: {ownership}{}", self.sw(¶m.ty))); let (arg, needs) = self.arg(¶m.ty, c, name, k, tr); arena |= needs; args.push(arg); @@ -1267,6 +1545,9 @@ impl<'a> Gen<'a> { } let mut text = function.docs.clone(); + for k in left { + text.push_str(&format!("\n\nWithout `{}`, as Rust's `None`.", unquote(&names[*k]))); + } for (param, name) in function.params.iter().zip(&names) { if let Type::Closure(lent, _) = ¶m.ty { text.push_str(&format!( @@ -1301,7 +1582,7 @@ impl<'a> Gen<'a> { let body = body.join("\n"); let var = function.params.is_empty() && !throws; let var = match (function.access, receiver) { - (Access::Field { set: false, .. }, Some(_)) => var, + (Access::Field { set: false, .. }, Some(_)) => var && mutation.is_empty(), (Access::Const, None) => { if var { member = member.trim_matches('`').to_uppercase(); @@ -1312,7 +1593,9 @@ impl<'a> Gen<'a> { }; let labels: Vec = match var { true => Vec::new(), - false => function.params.iter().map(|_| "_".to_string()).collect(), + false => (params.iter()) + .map(|p| unquote(p.split([' ', ':']).next().expect("a parameter has its label"))) + .collect(), }; // A member its type declares already, as a field of its name, takes a trailing `_`. match owner { @@ -1325,10 +1608,13 @@ impl<'a> Gen<'a> { } let head = match var { true => format!("public {stat}var {member}{}", ret.replacen(" -> ", ": ", 1)), - false => format!("public {stat}func {member}({}){effects}{ret}", params.join(", ")), + false => format!( + "public {stat}{mutation}func {member}({}){effects}{ret}", + params.join(", ") + ), }; let code = format!("{}{head} {{\n{}\n}}\n", docs(text.trim(), ""), indent(&body)); - Some((code, full(is_static, &member, labels))) + (code, full(is_static, &member, labels)) } /// The declaration of the local the call writes the out value `name` of C type `ty` to. @@ -1347,7 +1633,10 @@ impl<'a> Gen<'a> { let lifted: Vec = (elements.iter().enumerate()) .map(|(n, e)| self.lift(&strip_refs(e), &format!("c_out{n}_"))) .collect(); - format!("({})", lifted.join(", ")) + match self.unique(ok) { + true => format!("{}({})", self.sw(ok), lifted.join(", ")), + false => format!("({})", lifted.join(", ")), + } } _ => self.lift(ok, "c_out_"), } @@ -1394,17 +1683,77 @@ fn thrown_value(thrown: &Thrown, lifted: &str) -> String { } } -/// Whether passing `ty` moves an object's value out: by value, boxed, or in an `Option`, -/// a list, or a tuple, unless `Copy`, and never through a reference. -fn moved(abi: &Abi<'_>, ty: &Type) -> bool { +/// The most `Option<&T>` parameters of a struct a function takes, each doubling its +/// overloads. +const MAX_OPTIONAL: usize = 2; + +/// Each function or method Swift cannot export, with its span and why, which the driver +/// refuses before any language is generated: one taking more optional borrowed structs +/// than [`MAX_OPTIONAL`], or returning an error that holds one, which `Swift.Error` +/// requires be copyable. +pub fn refusals(abi: &Abi<'_>) -> Vec<(Span, String)> { + let structs = structs(abi); + let api = abi.api; + let methods = (api.records.iter().flat_map(|r| &r.methods)) + .chain(api.enums.iter().flat_map(|e| &e.methods)) + .chain(api.objects.iter().flat_map(|o| &o.methods)); + let mut refusals = Vec::new(); + for (kind, function) in (api.functions.iter().map(|f| ("fn", f))).chain(methods.map(|f| ("method", f))) { + let what = format!("cannot export {kind} `{}`", function.shown[1..].join("::")); + let optional = (function.params.iter()) + .filter(|p| lent_option(&structs, &p.ty)) + .count(); + if optional > MAX_OPTIONAL { + refusals.push(( + function.span, + format!( + "{what}: {optional} optional borrowed objects, past the {MAX_OPTIONAL} Swift takes, which overloads each with and without it since its optional holds an object only by consuming it" + ), + )); + } + if function.err.as_ref().is_some_and(|err| unique(&structs, err)) { + refusals.push(( + function.span, + format!("{what}: error holding an object Swift cannot copy, as `Swift.Error` requires"), + )); + } + } + refusals +} + +/// Whether Swift's value of `ty` is noncopyable: it holds one of `structs`. +fn unique(structs: &BTreeSet, ty: &Type) -> bool { match ty { - Type::Object(i) => !abi.api.objects[*i].copy, - Type::Option(inner) | Type::Box(inner) | Type::List(_, inner) => moved(abi, inner), - Type::Tuple(elements) => elements.iter().any(|ty| moved(abi, ty)), + Type::Object(i) => structs.contains(i), + Type::Option(inner) | Type::Box(inner) | Type::Ref(_, inner) | Type::List(_, inner) => unique(structs, inner), + Type::Tuple(elements) => elements.iter().any(|e| unique(structs, e)), _ => false, } } +/// Whether `ty` is an `Option<&T>` of one of `structs`. +fn lent_option(structs: &BTreeSet, ty: &Type) -> bool { + matches!(ty, Type::Option(inner) if matches!(&**inner, Type::Ref(Mutability::Not, object) if unique(structs, object))) +} + +/// The objects Swift holds as noncopyable structs: each but those `Copy`, and those thrown +/// as errors, which `Swift.Error` requires be copyable. +fn structs(abi: &Abi<'_>) -> BTreeSet { + let api = abi.api; + let methods = (api.records.iter().flat_map(|r| &r.methods)) + .chain(api.enums.iter().flat_map(|e| &e.methods)) + .chain(api.objects.iter().flat_map(|o| &o.methods)); + let thrown: BTreeSet = (api.functions.iter().chain(methods)) + .filter_map(|f| match f.err.as_ref().map(Type::base) { + Some(Type::Object(i)) => Some(*i), + _ => None, + }) + .collect(); + (0..api.objects.len()) + .filter(|i| !api.objects[*i].copy && !thrown.contains(i)) + .collect() +} + /// `ty` returned: a borrow is returned as a clone, so owned. fn strip_refs(ty: &Type) -> Type { match ty { diff --git a/ffi/tests/api.swift b/ffi/tests/api.swift index ab48a069b..cd36bc921 100644 --- a/ffi/tests/api.swift +++ b/ffi/tests/api.swift @@ -5,6 +5,7 @@ // breaks a contract, which must fail. import Api +import BasicContainers import Foundation // The Log callback: keeps even levels. @@ -36,6 +37,25 @@ func text(_ data: Data) -> String { String(decoding: data, as: UTF8.self) } +/// The refusal `refuse` throws for `reason`: a class, since Swift throws it. +func refusal(_ reason: String) -> Refusal { + do { + _ = try refuse(reason) + } catch let refusal as Refusal { + return refusal + } catch {} + fatalError("refuse returned") +} + +/// Each of `items` as `each` prints it, joined by commas. +func listed(_ items: borrowing UniqueArray, _ each: (borrowing T) -> String) -> String { + var out: [String] = [] + for i in 0.. String { var out = "" @@ -149,7 +169,7 @@ func script() { let d = Point.default() print("default \(d.x) \(d.y)") - let c = Counter.new("c", 5) + var c = Counter.new("c", 5) print("get \(c.get())") print("name \(c.name())") print("add ok \(try! c.add(10))") @@ -170,16 +190,17 @@ func script() { c.rename("d") print("name \(c.name())") print("is d \(c.`is`("d"))") - let copy = c.clone() + var copy = c.clone() _ = try! copy.add(1) print("clone \(copy.get()) original \(c.get())") - print("debug \(debug(c))") + print("debug \(c.debugDescription)") let label = c.intoLabel(p) print("label \(debug(label))") print("note \(hasText(Note(text: ""))) \(hasText(Note(text: nil)))") - let (below, above) = Counter.new("s", 7).split(5) - print("split \(below.name()) \(below.get()) \(above.name()) \(above.get())") + // A tuple holding a handle is a struct of its elements. + let split = Counter.new("s", 7).split(5) + print("split \(split._0.name()) \(split._0.get()) \(split._1.name()) \(split._1.get())") let one = only(Counter.new("o", 3)) print("only \(one.name()) \(one.get())") let (w8, w16, w32, w64) = widths(98305) @@ -208,10 +229,14 @@ func script() { } let statRows = rows([Point(x: 1, y: 2), Point(x: 3, y: 3)]) print("rows \(statRows.map { "\($0.0) \(debug($0.1)) \($0.2.x) \($0.2.y)" }.joined(separator: ", "))") - // Each handle in a tuple moves in, as one taken by value does. - let board = Board.new([("low", Counter.new("l", 1)), ("high", Counter.new("h", 9))]) + // Each handle in a tuple moves in, as one taken by value does, and an array of them is a + // `UniqueArray`. + var ranks = UniqueArray() + ranks.append(TupleStringCounter("low", Counter.new("l", 1))) + ranks.append(TupleStringCounter("high", Counter.new("h", 9))) + let board = Board.new(ranks) let ranked = board.ranked() - print("ranked \(ranked.map { "\($0.0) \($0.1.name()) \($0.1.get())" }.joined(separator: ", "))") + print("ranked \(listed(ranked) { "\($0._0) \($0._1.name()) \($0._1.get())" })") // A map crosses as its pairs and a set as its items, which Rust collects in order, each // key once, the last kept. let atlas = Atlas(points: [("b", Point(x: 1, y: 1)), ("ab", Point(x: 2, y: 3)), ("b", Point(x: 4, y: 4))], ids: [3, 1, 3]) @@ -245,13 +270,13 @@ func script() { print("title Some((\"\(title(true)!)\",)) \(title(false) == nil ? "None" : "Some") named Some((\"\(named("x")!)\",))") // Each lent counter is a new handle to a clone. - let registry = Registry.new(Counter.new("first", 1)) + var registry = Registry.new(Counter.new("first", 1)) print("last \(registry.last() != nil)") registry.push(Counter.new("last", 2)) - let first = registry.firstGet + var first = registry.firstGet _ = try! first.add(10) print("first \(registry.firstGet.get()) lent \(first.get())") - let last = registry.last()! + var last = registry.last()! _ = try! last.add(10) print("last \(registry.last()!.get()) lent \(last.get())") // A closure lent for the call edits the first counter through a handle. @@ -270,8 +295,8 @@ func script() { print("shift \(shift({ 7 }, 4))") let name = Name.owned("a") - let joined = name.join(Name.owned("b")) - print("joined \(joined.asStr()) \(debug(joined)) \(joined == name)") + var joined = name.join(Name.owned("b")) + print("joined \(joined.asStr()) \(joined.debugDescription) \(joined == name)") joined.rename("c") print("renamed \(joined.asStr())") let long = longer(joined, Name.owned("dd")) @@ -290,7 +315,7 @@ func script() { print("scaled \(scaledHex.joined(separator: " "))") check(U128(high: .max, low: .max).description == "340282366920938463463374607431768211455", "a u128's decimal digits") let sumHex = { (sum: ArrayU8x4) in sum.map { Swift.String(format: "%02x", $0) }.joined() } - let digest = Digest.new() + var digest = Digest.new() digest.sumSet(try! fingerprint("deadbeef")) var unparsed = "" do { @@ -302,11 +327,11 @@ func script() { } print("digest \(sumHex(digest.sumGet)) \(sumHex(digest.flipped(ArrayU8x4([0xff, 0, 0xff, 0])!))) \(unparsed)") check(ArrayU8x4([1, 2, 3]) == nil, "3 bytes are no [u8; 4]") - let batch = Batch.new(), batch2 = pair() + var batch = Batch.new(), batch2 = pair() let accepted = (1...5).filter { batch.push($0) }.count let paired = (1...3).filter { batch2.push($0) }.count - let emptied = drain4(batch) - print("batch \(batch.capacity()) \(accepted) \(emptied) \(drain4(batch)) \(batch2.capacity()) \(paired) \(drain2(batch2))") + let emptied = drain4(&batch) + print("batch \(batch.capacity()) \(accepted) \(emptied) \(drain4(&batch)) \(batch2.capacity()) \(paired) \(drain2(&batch2))") print("capacities \(capacities42(batch, batch2)) \(capacities22(batch2, batch2))") let wide = widen(RangeU64(start: 2, end: 5), 1) print("widen \(wide.start) \(wide.end)") @@ -360,7 +385,7 @@ func script() { print("secs \(stamp.secs())") print("later \(Stamp.fromMillis(3000).later(stamp))") print("secs \(stamp.secs())") - let message = Message.new(stamp, Data("body".utf8)) + var message = Message.new(stamp, Data("body".utf8)) print("payload \(text(message.payloadGet))") print("stamp \(debug(message.stampGet!))") print("sent \(message.sent())") @@ -411,7 +436,8 @@ func script() { } catch { print("nonempty err \(error)") } - let filled = try await fill4(Batch.new(), 9) + fill2(Api.pair(), 1) + var four = Batch.new(), two = Api.pair() + let filled = try await fill4(&four, 9) + fill2(&two, 1) print("fill \(filled.map { Swift.String($0) }.joined(separator: " "))") // The send happens at the call, before the future is first polled. try await channel.sendLater(Data("eager".utf8)) @@ -427,8 +453,10 @@ func script() { block { // A poll with no async twin is a future. print("take \(text(try await channel.pollTake()))") - channel.send(Data("measured".utf8)) - print("measure \(try await channel.pollMeasure(2))") + // A `&mut self` poll needs a binding of its own, which a closure cannot capture. + var measured = Channel.new() + measured.send(Data("measured".utf8)) + print("measure \(try await measured.pollMeasure(2))") } // A context is made of the waker, which Rust keeps until the send wakes it. channel.listen { wakes.value += 1 } @@ -442,7 +470,7 @@ func script() { } print("woken \(wakes.value)") - let list = Node.new(1, nil) + var list = Node.new(1, nil) list.push(2) print("node \(list.sum())") @@ -468,7 +496,7 @@ func script() { let beside = PairU32(left: 258, right: 9).besideSize(sized.left) print("beside \(beside.left) \(debug(beside))") // At its default, named as the type itself. - let stack = Stack.new() + var stack = Stack.new() for (i, n) in ["a", "b"].enumerated() { stack.push(Counter.new(n, UInt64(i) + 1)) } @@ -526,7 +554,7 @@ func script() { print("dropped \(dropped())") // A field and the method of its name, each its own member. - let retry = Retry.new(5) + var retry = Retry.new(5) let limit = retry.limitGet print("retry \(limit != nil ? 1 : 0) \(limit ?? 0) \(retry.limit()) \(retry.delay())") // The renamed field's setter, named as its getter. @@ -536,9 +564,9 @@ func script() { // Foreign types: a URL parsed from text, socket addresses, OS paths, and `Vec`s. let base = try! Url.parse("https://example.com/a/b") - print("url \(base) host \(host(base).map { "Some(\"\($0)\")" } ?? "None")") + print("url \(base.description) host \(host(base).map { "Some(\"\($0)\")" } ?? "None")") let url = try! resolve(base.clone(), "c?d") - print("resolve \(url) \(url == base)") + print("resolve \(url.description) \(url == base)") do { _ = try Url.parse("no scheme") } catch let e as StringError { @@ -563,10 +591,28 @@ func script() { print("prefixed \(String(decoding: prefixed(Data("cd".utf8), Data("ab".utf8)), as: UTF8.self)) \(String(decoding: prefixed(Data("cd".utf8), nil), as: UTF8.self))") print("coordinates \(coordinates(Point(x: 2, y: 3))) \(coordinates(nil))") do { + // An optional borrowed handle is an overload with it and one without; one borrowed + // mutably is an optional `inout`. let two = Counter.new("two", 2) - let three = Counter.new("three", 3) - print("plus \(two.plus(three)) \(two.plus(nil))") - print("add_to \(two.addTo(three)) \(two.addTo(nil)) \(three.get())") + var three: Counter? = Counter.new("three", 3) + var none: Counter? = nil + print("plus \(two.plus(other: three!)) \(two.plus())") + // A list and an option holding a struct beside a class, whose handle each moves. + var pairs = UniqueArray() + pairs.append(TupleCounterRefusal(Counter.new("a", 1), refusal("x"))) + check( + reasons(pairs, TupleCounterRefusal(Counter.new("b", 2), refusal("y"))) + == "a refused: x, b refused: y", + "reasons moves a struct and a class out of a list and an option" + ) + // Each optional borrowed parameter is an overload with and without its label. + let one = Counter.new("one", 1) + check( + two.clamped() == 2 && two.clamped(low: three!) == 3 && two.clamped(high: one) == 1 + && two.clamped(low: one, high: three!) == 2, + "clamped overloads each optional borrow" + ) + print("add_to \(two.addTo(&three)) \(two.addTo(&none)) \(three!.get())") } do { var encoded = Data("0,".utf8) @@ -588,7 +634,7 @@ func script() { do { let config = secure("h3") print("secure \(alpnLent(config)) \(alpn(config.clone()))") - let meters = Meters.new() + var meters = Meters.new() let first = meters.first() _ = first.add(2) print("lent \(first.add(3)) \(meters.first().count())") @@ -599,10 +645,14 @@ func script() { print("tallied \(tallies.first().bump())") } print("chunks \(chunks(Data("abcde".utf8), 2).map { Array($0) })") - let counters = counters(["x", "y"]) - print("counters \(counters.map { "\($0.name()) \($0.get())" }.joined(separator: ", "))") - // Each counter moves in, so Swift closes it. - print("worth \(worth([counters[0]], [counters[1]])) \(worth(nil, []))") + var counters = counters(["x", "y"]) + print("counters \(listed(counters) { "\($0.name()) \($0.get())" })") + // Each counter moves in. + var some = UniqueArray() + some.append(counters.remove(at: 0)) + var boxed = UniqueArray() + boxed.append(counters.remove(at: 0)) + print("worth \(worth(some, boxed)) \(worth(nil, UniqueArray()))") print("held \([heldCount(Counter.new("h", 4)), heldCount(nil)].map { $0.map { "Some(\($0))" } ?? "None" }.joined(separator: " "))") print("boxed \(boxedName(boxedCounter("b")).map { "Some(\"\($0)\")" } ?? "None") \(boxedName(nil) ?? "None") \(boxedCounter(nil) == nil)") print("less \(less(5, 2))") @@ -625,8 +675,11 @@ func script() { let tagged = tagged(doc, "t") print("tagged \(tagged.toText())") // A JSON object's entries cross as pairs of a key and its document, each moved in. - let jsonEntries = entries([("b", try! Json.parse("[1]")), ("a", try! Json.parse("2"))], "t") - print("entries \(jsonEntries.map { "\($0.0) \($0.1.toText())" }.joined(separator: ", "))") + var pairs = UniqueArray() + pairs.append(TupleStringJson("b", try! Json.parse("[1]"))) + pairs.append(TupleStringJson("a", try! Json.parse("2"))) + let jsonEntries = entries(pairs, "t") + print("entries \(listed(jsonEntries) { "\($0._0) \($0._1.toText())" })") do { _ = try Json.parse("{\"a\":") } catch let e as StringError { @@ -635,11 +688,11 @@ func script() { check(false, "parse throws its message") } let site = Site.new(base) - print("site \(site.homeGet) \(site.homeGet)") + print("site \(site.homeGet.description) \(site.homeGet.description)") // Slices, passed in and returned as clones, and the socket address of each family. print("shapes \(Shape.ALL) greetings \(GREETINGS)") - let trail = Trail.new([Point(x: 1, y: 2), Point(x: 3, y: 4)], ["a", "b"]) + var trail = Trail.new([Point(x: 1, y: 2), Point(x: 3, y: 4)], ["a", "b"]) print("trail \(trail.points()) \(trail.labels())") // Returned iterators, each collected at the call: drained labels are gone after. print("iterated \(trail.iter()) \(trail.names()) \(Array(trail.bytes()))") @@ -647,20 +700,23 @@ func script() { // A filter, collected at the call. print("beyond \(trail.beyond(1).map { "\($0.x) \($0.y)" }.joined(separator: ", "))") let numbered = numbered(["p", "q"]) - print("numbered \(numbered.map { "\($0.name()) \($0.get())" }.joined(separator: ", "))") + print("numbered \(listed(numbered) { "\($0.name()) \($0.get())" })") let (next4, next6) = nexts(SocketAddr(ip: [127, 0, 0, 1], port: 8080), v6) print("nexts \(next4.ip.map(String.init).joined(separator: ".")) \(next4.port) \(next6.port) \(next6.scopeId)") // serde_json's map iterator, collected as pairs of a key and its document. - let jsonSections = sections([("b", try! Json.parse("[1]")), ("a", try! Json.parse("2"))]) - print("sections \(jsonSections.map { "\($0.0) \($0.1.toText())" }.joined(separator: ", "))") + var sectionPairs = UniqueArray() + sectionPairs.append(TupleStringJson("b", try! Json.parse("[1]"))) + sectionPairs.append(TupleStringJson("a", try! Json.parse("2"))) + let jsonSections = sections(sectionPairs) + print("sections \(listed(jsonSections) { "\($0._0) \($0._1.toText())" })") // An instant only Rust makes, which each call copies out of its handle, and compares. let millis = { (d: Swift.Duration) in d.components.seconds * 1000 + d.components.attoseconds / 1_000_000_000_000_000 } let began = now() let ended = later(began, .milliseconds(1500)) print("instants \(millis(since(ended, began))) \(millis(since(began, ended))) \(began == later(began, .zero)) \(began == ended)") // tracing's level, and its filter as an optional level whose nil is `OFF`. - let verbosity = Verbosity.new(.warn) + var verbosity = Verbosity.new(.warn) print("verbosity \(spell(verbosity.filter())) \(spell(louder(verbosity.filter())))") verbosity.levelSet(.trace) print("verbosity \(verbosity.levelGet == .trace) \(spell(verbosity.filter())) \(spell(louder(verbosity.filter()))) \(spell(louder(nil)))") @@ -769,12 +825,14 @@ func futures() { // An object whose last reference goes while a call awaits on it stays alive until the // call ends. do { - let other = Cell(Channel.new()) - let sender = other.value!.clone() let got = Cell(nil) - q.launch { got.value = text(try await other.value!.recv()) } - q.drain() - other.value = nil + let sender: Channel + do { + let other = Channel.new() + sender = other.clone() + q.launch { got.value = text(try await other.recv()) } + q.drain() + } sender.send(Data("held".utf8)) q.drain() check(got.value == "held", "the call awaiting on an object holds it") @@ -811,39 +869,58 @@ func owned() { print("owned ok") } -/// Holds a lent counter past the call it was lent for. +// Each breaks a contract Swift's compiler checks, so it fails to compile, built alone with +// its flag. +#if MOVED +func moved() { + let c = Counter.new("c", 0) + _ = c.intoLabel(nil) + _ = c.get() +} +#endif + +#if BOXED_MOVED +func boxedMoved() { + let tail = Node.new(1, nil) + _ = Node.new(2, tail) + _ = tail.sum() +} +#endif + +#if LENT_MOVED +func lentMoved() { + var registry = Registry.new(Counter.new("c", 0)) + _ = registry.edit { counter in + _ = counter.intoLabel(nil) + return false + } +} +#endif + +#if LENT_KEPT +/// Holds a lent tally past the call it was lent for. final class Kept: @unchecked Sendable { - var counter: Counter? + var tally: Tally? } +func lentKept() { + let kept = Kept() + _ = peek(Tally.new()) { lent in + kept.tally = lent + return 0 + } +} +#endif + // Each breaks a contract, which must fail with a message naming it. func broken(_ which: String) { switch which { - case "moved": - let c = Counter.new("c", 0) - _ = c.intoLabel(nil) - _ = c.get() - case "boxed moved": - let tail = Node.new(1, nil) - _ = Node.new(2, tail) - _ = tail.sum() + // A lent return outliving its owner's change, which Rust checks. case "outlived": - let meters = Meters.new() + var meters = Meters.new() let first = meters.first() meters.clear() _ = first.count() - case "lent moved": - _ = Registry.new(Counter.new("c", 0)).edit { counter in - _ = counter.intoLabel(nil) - return false - } - case "lent kept": - let kept = Kept() - _ = Registry.new(Counter.new("c", 0)).edit { counter in - kept.counter = counter - return false - } - _ = kept.counter!.get() // An IPv6 address where Rust takes `SocketAddrV4`. case "family": let any = SocketAddr(ip: Array(repeating: 0, count: 16), port: 1) @@ -853,6 +930,13 @@ func broken(_ which: String) { _ = stray() case "lent stray": visitStray { _ in } + // A class moved into Rust, which Swift still holds: the call closes its handle. + case "refusal moved": + let kept = refusal("z") + var pairs = UniqueArray() + pairs.append(TupleCounterRefusal(Counter.new("a", 1), kept)) + _ = reasons(pairs, nil) + _ = kept.description // The file serves wasm32 too, whose items the host's library lacks. case "target": _ = wasmOnly() diff --git a/ffi/tests/fixtures.rs b/ffi/tests/fixtures.rs index b56efb18b..a3384794b 100644 --- a/ffi/tests/fixtures.rs +++ b/ffi/tests/fixtures.rs @@ -710,7 +710,8 @@ fn kotlin_contract(kotlin: &Path) -> u64 { /// The generated Swift, built as the module `Api` with warnings as errors in Swift 6's /// language mode and type-checked in Swift 5's, which moq's package builds in, runs /// `tests/api.swift`'s script, its futures, and what releasing frees, then fails loud on -/// each broken contract and on a library of another contract. +/// each broken contract and on a library of another contract. Each contract Swift's +/// compiler checks, a move or a borrow, fails to compile instead. #[test] #[cfg_attr( all(target_os = "linux", target_arch = "aarch64"), @@ -723,6 +724,7 @@ fn api_swift() { std::fs::create_dir_all(&dir).unwrap(); let module = api.out.join("swift"); let source = module.join("api.swift"); + let collections = swift_collections(); run(swiftc(&module, 13) .args(["-swift-version", "5", "-typecheck"]) .arg(&source)); @@ -735,11 +737,13 @@ fn api_swift() { .arg(&source)); // The test's executor preference is macOS 15's. let exe = dir.join("api-test"); + let test = manifest_dir().join("tests").join("api.swift"); run(swiftc(&module, 15) .args(["-parse-as-library", "-I"]) .arg(&dir) - .arg(manifest_dir().join("tests").join("api.swift")) + .arg(&test) .arg(dir.join("libApi.a")) + .args(&collections) .arg(api.staticlib()) .args(&api.native) .arg("-o") @@ -752,18 +756,36 @@ fn api_swift() { ); run(Command::new(&exe).arg("futures")); run(Command::new(&exe).arg("owned")); + // Each in a build of its own, since the first error stops the rest; move checking runs + // on SIL, which type-checking alone skips. for (case, message) in [ - ("moved", "Counter is closed"), - ("boxed moved", "Node is closed"), - ("lent kept", "Counter is closed"), + ("MOVED", "'c' used after consume"), + ("BOXED_MOVED", "'tail' used after consume"), ( - "lent moved", - "api_Counter_into_label: a lent handle's value cannot move out", + "LENT_MOVED", + "missing reinitialization of inout parameter 'counter' after consume", ), + ("LENT_KEPT", "implicit conversion to 'Tally?' is consuming"), + ] { + let output = swiftc(&module, 15) + .args(["-parse-as-library", "-emit-sil", "-o"]) + .arg(dir.join("broken.sil")) + .args(["-I"]) + .arg(&dir) + .arg(format!("-D{case}")) + .arg(&test) + .output() + .unwrap(); + assert!(!output.status.success(), "Swift {case} compiled"); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains(message), "Swift {case} printed {stderr}"); + } + for (case, message) in [ ("family", "api_nexts: an IPv6 socket address where IPv4 is taken"), ("outlived", OUTLIVED), ("stray", "a path from Rust is not UTF-8"), ("lent stray", "a path from Rust is not UTF-8"), + ("refusal moved", "Refusal is closed"), ("target", "api_wasm_only: it is not on this library's target"), ("variant", "api_Platform.Wasm: it is not on this library's target"), ("plain variant", "api_Family.Wasm: it is not on this library's target"), @@ -793,6 +815,7 @@ fn api_swift() { .args(["-module-name", "Api"]) .arg(contract.join("api.swift")) .arg(contract.join("main.swift")) + .args(&collections) .arg(api.staticlib()) .args(&api.native) .arg("-o") @@ -953,8 +976,27 @@ fn url(dir: &Path) -> String { } } -/// swiftc, with warnings as errors, finding the C header's module in `module`, at this -/// macOS deployment target on macOS: 13 is the oldest the generated `Swift.Duration` allows. +/// The static libraries of swift-collections' modules the generated Swift imports, which +/// `just ffi swift-collections` builds beside their modules, where [`swiftc`] finds them. +fn swift_collections() -> Vec { + run(Command::new("just") + .arg("--justfile") + .arg(manifest_dir().join("justfile")) + .arg("swift-collections")); + let build = collections_dir(); + ["BasicContainers", "InternalCollectionsUtilities", "SpanPreview"] + .iter() + .map(|module| build.join(format!("lib{module}.a"))) + .collect() +} + +fn collections_dir() -> PathBuf { + manifest_dir().join("target").join("swift-collections").join("build") +} + +/// swiftc, with warnings as errors, finding the C header's module in `module` and +/// swift-collections' in [`collections_dir`], at this macOS deployment target on macOS: 13 +/// is the oldest the generated `Swift.Duration` allows. fn swiftc(module: &Path, macos: u8) -> Command { let mut cmd = Command::new("swiftc"); if cfg!(target_os = "macos") { @@ -964,7 +1006,10 @@ fn swiftc(module: &Path, macos: u8) -> Command { }; cmd.arg("-target").arg(format!("{arch}-apple-macos{macos}")); } - cmd.args(["-warnings-as-errors", "-I"]).arg(module); + cmd.args(["-warnings-as-errors", "-I"]) + .arg(module) + .arg("-I") + .arg(collections_dir()); cmd } diff --git a/quest/m1/ffi-export/swift-noncopyable.md b/quest/m1/ffi-export/swift-noncopyable.md deleted file mode 100644 index abd51c0c2..000000000 --- a/quest/m1/ffi-export/swift-noncopyable.md +++ /dev/null @@ -1,59 +0,0 @@ -# [L] Swift handles are non-copyable - -## Goal - -[rust2-ffi](/ffi/README.md)'s Swift generator emits each object handle (a type that is not -`Copy` and not an error) as a `~Copyable` struct, with `consuming`, `borrowing`, -`mutating`, and `inout` where Rust takes `self`, `&self`, `&mut self`, and `&mut`. A call on -a moved handle, or a lent handle kept past its call, is a compile error where it traps today. -Proven by `ffi/tests/api.swift`: its `moved`, `boxed moved`, `lent moved`, and `lent kept` -cases fail to compile, and the rest of the script still matches the native transcript, on -Linux and on an Apple platform. - -Errors stay classes, since `Swift.Error` requires `Copyable`. `Sendable` is -[not-send](/quest/m1/ffi-export/not-send.md)'s, and the other languages are out of scope. - -## Plan - -Decided with the maintainer on 2026-11-09, from the spike -[moq-dev/rust2#688](https://github.com/moq-dev/rust2/pull/688), whose PR body has the -report and whose [evidence comment](https://github.com/moq-dev/rust2/pull/688#issuecomment-6758972634) -has the scripts that generated the stricter forms: - -- ✅ Switch handles to `~Copyable` structs, with tuples holding a handle as generated named - structs and arrays as swift-collections' `UniqueArray` (rejected: a generated list type - of rust2's own; waiting for Swift's own `Array` to hold noncopyable values; no switch). - Swift 6.3 holds a noncopyable value in neither, which ruled out 9 of the api fixture's - exports in the spike. The generated module gains the swift-collections dependency. -- ✅ Drop the per-handle atomic hold count once a release-build measurement confirms the - gain (rejected: keeping it as a backstop). The compiler then refuses everything it - guarded; the one case left, a data race in Swift 5 mode, is already undefined behavior - in Swift. Measure it on [the binding benchmark](/bench/ffi/README.md), whose Swift handle - call is the baseline. The spike measured `get()` at 24.6 ns with it and 17.9 ns without, against a - debug library on a loaded host. This is the Swift side of - [checks-raise](/quest/m1/ffi-export/checks-raise.md)'s one accounting. -- ✅ m1, in the exporter epic (rejected: m2), since the switch breaks the generated Swift - API, which is cheapest before moq adopts the bindings. -- ✅ An `Option<&T>` parameter gets an overload per optional borrowed parameter, one taking - the borrowed handle and one without it (rejected: one `borrowing T?` signature, which - makes a caller holding a plain handle move it into an optional and back; a generated - `Borrowed` view built at each call). Wrapping a borrowed handle in an optional consumes - it. moq's items have one such parameter each; refuse past a small bound at the item's span. -- Swift 6.4 conformances (`Equatable`, `Hashable`, `CustomStringConvertible`, - `CustomDebugStringConvertible` for noncopyable types, from - [SE-0499](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0499-support-non-copyable-simple-protocols.md)) - come in under `#if compiler(>=6.4)`. Until then `debug(c)` is `c.debugDescription` and - `"\(url)"` is `url.description`; `==` stays a static operator. -- A `&mut self` call needs the handle in a `var`. -- Check on an Apple platform that `Optional<~Copyable>` metadata back-deploys to the - deployment target the generated package declares; the spike ran on Linux only. -- [moq-dev/rust2#678](https://github.com/moq-dev/rust2/pull/678)'s `&mut` in/out becomes - `inout`, exclusive at compile time, and `Option<&mut T>` becomes `inout T?`. Whichever - lands second adapts to the other. - -Public API: every generated Swift handle type and the signatures taking one (breaking), and -the generated package's swift-collections dependency. - -## Related - -- [Failed checks raise](/quest/m1/ffi-export/checks-raise.md) - the run-time checks left after this switch raise rather than trap