From 36b2a73637fc685ae839de2c8abc7786dd09bd85 Mon Sep 17 00:00:00 2001 From: Mihaela Mihaljevic Date: Mon, 29 Jun 2026 15:12:02 +0200 Subject: [PATCH] chore(concurrency): justify every @unchecked Sendable / nonisolated(unsafe) (#387) The rules-swift concurrency rule's acceptance check requires every `@unchecked Sendable` and `nonisolated(unsafe)` to carry an adjacent comment explaining why the race is acceptable; an unjustified one is a violation. An audit found six escape hatches whose comment explained the type but not the race: - The four XPath `FunctionTable` statics (`CoreFunctions`, `StringFunctions`, `NodeFunctions`, `Evaluator.library`): each is an immutable, build-once lookup table never mutated after initialization, so concurrent reads race on nothing; `FunctionTable` is un-`Sendable` only because it stores closures. - `Validation.Validator`: immutable after `init` (every stored validation list is a `let`); the erased `AnyValidation` closures are simply not provably `Sendable`. - The test `ResultBox`: written once by the worker thread before its semaphore signals and read only after the wait, so the write happens-before the read. Comments only; no behaviour change. `LoaderBox` and `ElementStorage` were already justified. swiftformat/swiftlint --strict and the full suite stay green. --- Sources/Validation/Validator.swift | 4 ++++ Sources/XPath/CoreFunctions.swift | 3 +++ Sources/XPath/Evaluator.swift | 5 ++++- Sources/XPath/NodeFunctions.swift | 3 +++ Sources/XPath/StringFunctions.swift | 3 +++ Tests/SchemaContentModelDepthSafetyTests.swift | 3 +++ 6 files changed, 20 insertions(+), 1 deletion(-) diff --git a/Sources/Validation/Validator.swift b/Sources/Validation/Validator.swift index 8a8ecc1..a635dcc 100644 --- a/Sources/Validation/Validator.swift +++ b/Sources/Validation/Validator.swift @@ -8,6 +8,10 @@ public extension PureXML.Validation { /// /// Parameterized over `Document`, the cross-cutting context the rules consult /// (a schema, a scope, or `Void` for rules that need only their subject). + /// + /// `@unchecked Sendable`: immutable after `init` (every stored validation list is + /// a `let`), so concurrent reads race on nothing; the erased `AnyValidation` + /// closures are simply not provably `Sendable` to the compiler. final class Validator: @unchecked Sendable { fileprivate let nonReferenceDefaultValidations: [AnyValidation] fileprivate let referenceDefaultValidations: [AnyValidation] diff --git a/Sources/XPath/CoreFunctions.swift b/Sources/XPath/CoreFunctions.swift index 124d9cf..1df583e 100644 --- a/Sources/XPath/CoreFunctions.swift +++ b/Sources/XPath/CoreFunctions.swift @@ -4,6 +4,9 @@ extension PureXML.XPath { /// and `string`. The broader library (string, node, and number functions) /// extends this table. enum CoreFunctions { + // nonisolated(unsafe): an immutable, build-once table never mutated after + // initialization, so concurrent reads are safe; `FunctionTable` is only + // un-`Sendable` because it stores closures. nonisolated(unsafe) static let table = FunctionTable([ "last": { _, context in .number(Double(context.size)) }, "position": { _, context in .number(Double(context.position)) }, diff --git a/Sources/XPath/Evaluator.swift b/Sources/XPath/Evaluator.swift index e05c589..a0749fe 100644 --- a/Sources/XPath/Evaluator.swift +++ b/Sources/XPath/Evaluator.swift @@ -4,7 +4,10 @@ extension PureXML.XPath { /// grammar, functions, and variables build on the four-type model. enum Evaluator { /// The full XPath 1.0 function library: the core functions plus the - /// string, node, and number families. + /// string, node, and number families. `nonisolated(unsafe)` is safe here: an + /// immutable, build-once table never mutated after initialization, so + /// concurrent reads race on nothing; `FunctionTable` is un-`Sendable` only + /// because it stores closures. nonisolated(unsafe) static let library = CoreFunctions.table .merging(StringFunctions.table) .merging(NodeFunctions.table) diff --git a/Sources/XPath/NodeFunctions.swift b/Sources/XPath/NodeFunctions.swift index c17afe0..6f783a9 100644 --- a/Sources/XPath/NodeFunctions.swift +++ b/Sources/XPath/NodeFunctions.swift @@ -7,6 +7,9 @@ extension PureXML.XPath { /// ID, so `id()` matches elements by an attribute named `id` or `xml:id`, the /// near-universal convention. enum NodeFunctions { + // nonisolated(unsafe): an immutable, build-once table never mutated after + // initialization, so concurrent reads are safe; `FunctionTable` is only + // un-`Sendable` because it stores closures. nonisolated(unsafe) static let table = FunctionTable([ "local-name": { arguments, context in .string(named(arguments, context)?.localName ?? "") }, "name": { arguments, context in .string(named(arguments, context)?.description ?? "") }, diff --git a/Sources/XPath/StringFunctions.swift b/Sources/XPath/StringFunctions.swift index 27e6fe0..f078610 100644 --- a/Sources/XPath/StringFunctions.swift +++ b/Sources/XPath/StringFunctions.swift @@ -3,6 +3,9 @@ extension PureXML.XPath { /// `substring-before`, `substring-after`, `substring`, `string-length`, /// `normalize-space`, and `translate`. enum StringFunctions { + // nonisolated(unsafe): an immutable, build-once table never mutated after + // initialization, so concurrent reads are safe; `FunctionTable` is only + // un-`Sendable` because it stores closures. nonisolated(unsafe) static let table = FunctionTable([ "concat": { arguments, _ in .string(arguments.map(\.string).joined()) }, "starts-with": { arguments, _ in diff --git a/Tests/SchemaContentModelDepthSafetyTests.swift b/Tests/SchemaContentModelDepthSafetyTests.swift index 975e6bc..da930f3 100644 --- a/Tests/SchemaContentModelDepthSafetyTests.swift +++ b/Tests/SchemaContentModelDepthSafetyTests.swift @@ -128,6 +128,9 @@ return result.outcome } + /// @unchecked Sendable: written once by the worker thread before its semaphore + /// signals and read only after `runOnNativeStack`'s wait, so the worker's write + /// happens-before the read and there is no concurrent access. private final class ResultBox: @unchecked Sendable { var outcome = Outcome(compiled: false, findings: []) }