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.
This commit is contained in:
parent
41338c9e3f
commit
36b2a73637
@ -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<Document>: @unchecked Sendable {
|
||||
fileprivate let nonReferenceDefaultValidations: [AnyValidation<Document>]
|
||||
fileprivate let referenceDefaultValidations: [AnyValidation<Document>]
|
||||
|
||||
@ -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)) },
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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 ?? "") },
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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: [])
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user