From 0b1fe4ac0ef263af81ef7fc49a6f8d3973fb8dbd Mon Sep 17 00:00:00 2001 From: Nate Cook Date: Tue, 22 Sep 2026 16:29:08 -0500 Subject: [PATCH 1/2] Make References concurrency safe --- Sources/_StringProcessing/Regex/DSLTree.swift | 6 +- .../Utility/AtomicCounter.swift | 39 +++++ .../ReferenceConcurrencyTests.swift | 144 ++++++++++++++++++ 3 files changed, 186 insertions(+), 3 deletions(-) create mode 100644 Sources/_StringProcessing/Utility/AtomicCounter.swift create mode 100644 Tests/RegexBuilderTests/ReferenceConcurrencyTests.swift diff --git a/Sources/_StringProcessing/Regex/DSLTree.swift b/Sources/_StringProcessing/Regex/DSLTree.swift index 36c4baf29..6c1be8032 100644 --- a/Sources/_StringProcessing/Regex/DSLTree.swift +++ b/Sources/_StringProcessing/Regex/DSLTree.swift @@ -416,7 +416,8 @@ extension DSLTree.Node { @_spi(RegexBuilder) public struct ReferenceID: Hashable { - nonisolated(unsafe) private static var counter: Int = 0 + private static let counter = AtomicCounter(startingAt: 0) + var base: Int public var _raw: Int { @@ -424,8 +425,7 @@ public struct ReferenceID: Hashable { } public init() { - base = Self.counter - Self.counter += 1 + base = Self.counter.next() } init(_ base: Int) { diff --git a/Sources/_StringProcessing/Utility/AtomicCounter.swift b/Sources/_StringProcessing/Utility/AtomicCounter.swift new file mode 100644 index 000000000..dbd184dcb --- /dev/null +++ b/Sources/_StringProcessing/Utility/AtomicCounter.swift @@ -0,0 +1,39 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +/// A thread-safe integer counter. +/// +/// This is built on `_swift_stdlib_atomicFetchAddInt`, an underscored +/// public entry point that the standard library has exported since ABI +/// stability. +final class AtomicCounter: @unchecked Sendable { + /// The counter's storage, on the heap to provide a stable address. + private let storage: UnsafeMutablePointer + + init(startingAt value: Int = 0) { + storage = UnsafeMutablePointer.allocate(capacity: 1) + storage.initialize(to: value) + } + + deinit { + storage.deallocate() + } + + /// The current value. + var value: Int { + _swift_stdlib_atomicLoadInt(object: storage) + } + + /// Returns the current value and increments it. + func next() -> Int { + _swift_stdlib_atomicFetchAddInt(object: storage, operand: 1) + } +} diff --git a/Tests/RegexBuilderTests/ReferenceConcurrencyTests.swift b/Tests/RegexBuilderTests/ReferenceConcurrencyTests.swift new file mode 100644 index 000000000..bb29dd889 --- /dev/null +++ b/Tests/RegexBuilderTests/ReferenceConcurrencyTests.swift @@ -0,0 +1,144 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + + +import XCTest +import Dispatch +@testable import _StringProcessing +@testable import RegexBuilder + +@available(SwiftStdlib 5.7, *) +class ReferenceConcurrencyTests: XCTestCase { + static let threadCount = 8 +} + +#if canImport(Dispatch) + +extension ReferenceConcurrencyTests { + + func testAtomicIntsAreUnique() { + let perThread = 2_000 + let total = Self.threadCount * perThread + let counter = AtomicCounter() + + nonisolated(unsafe) + let ids = UnsafeMutablePointer.allocate(capacity: total) + defer { + ids.deallocate() + } + + DispatchQueue.concurrentPerform(iterations: Self.threadCount) { thread in + let base = thread * perThread + for i in 0..>.allocate(capacity: total) + defer { + refs.deinitialize(count: total) + refs.deallocate() + } + + DispatchQueue.concurrentPerform(iterations: Self.threadCount) { thread in + let base = thread * perThread + for i in 0.. String? { + let group1 = Reference(Substring.self) + let group2 = Reference(Substring.self) + let group3 = Reference(Substring.self) + let group4 = Reference(Substring.self) + let group5 = Reference(Substring.self) + let group6 = Reference(Substring.self) + let group7 = Reference(Substring.self) + let group8 = Reference(Substring.self) + + let regex = Regex { + Capture(as: group1) { OneOrMore(.hexDigit) } + ":" + Capture(as: group2) { OneOrMore(.hexDigit) } + ":" + Capture(as: group3) { OneOrMore(.hexDigit) } + ":" + Capture(as: group4) { OneOrMore(.hexDigit) } + ":" + Capture(as: group5) { OneOrMore(.hexDigit) } + ":" + Capture(as: group6) { OneOrMore(.hexDigit) } + ":" + Capture(as: group7) { OneOrMore(.hexDigit) } + ":" + Capture(as: group8) { OneOrMore(.hexDigit) } + } + + guard let m = try? regex.wholeMatch(in: input) else { + return "wholeMatch returned nil" + } + + if m[group1] != m.1 || m[group2] != m.2 || m[group3] != m.3 || m[group4] != m.4 || + m[group5] != m.5 || m[group6] != m.6 || m[group7] != m.7 || m[group8] != m.8 + { + return """ + crossed captures: + \(m[group1]):\(m[group2]):\(m[group3]):\(m[group4]):\ + \(m[group5]):\(m[group6]):\(m[group7]):\(m[group8]) + """ + } + return nil + } + + let iterations = 500_000 + let failures = await withTaskGroup(of: String?.self) { group in + for _ in 0..() async rethrows -> [T] where Element == T? { + try await reduce(into: []) { result, el in if let el { result.append(el) } } + } +} From ee710e148182db1d303919f6e0c6e8bd81015ac2 Mon Sep 17 00:00:00 2001 From: Nate Cook Date: Tue, 29 Sep 2026 13:40:34 -0500 Subject: [PATCH 2/2] Switch to ManagedBuffer to get rid of extra alloc --- Sources/_StringProcessing/Regex/DSLTree.swift | 2 +- .../Utility/AtomicCounter.swift | 46 +++++++++++++------ 2 files changed, 33 insertions(+), 15 deletions(-) diff --git a/Sources/_StringProcessing/Regex/DSLTree.swift b/Sources/_StringProcessing/Regex/DSLTree.swift index 6c1be8032..2449bfe0e 100644 --- a/Sources/_StringProcessing/Regex/DSLTree.swift +++ b/Sources/_StringProcessing/Regex/DSLTree.swift @@ -416,7 +416,7 @@ extension DSLTree.Node { @_spi(RegexBuilder) public struct ReferenceID: Hashable { - private static let counter = AtomicCounter(startingAt: 0) + private static let counter = AtomicCounter() var base: Int diff --git a/Sources/_StringProcessing/Utility/AtomicCounter.swift b/Sources/_StringProcessing/Utility/AtomicCounter.swift index dbd184dcb..31b34b6a6 100644 --- a/Sources/_StringProcessing/Utility/AtomicCounter.swift +++ b/Sources/_StringProcessing/Utility/AtomicCounter.swift @@ -14,26 +14,44 @@ /// This is built on `_swift_stdlib_atomicFetchAddInt`, an underscored /// public entry point that the standard library has exported since ABI /// stability. -final class AtomicCounter: @unchecked Sendable { - /// The counter's storage, on the heap to provide a stable address. - private let storage: UnsafeMutablePointer - - init(startingAt value: Int = 0) { - storage = UnsafeMutablePointer.allocate(capacity: 1) - storage.initialize(to: value) +struct AtomicCounter: @unchecked Sendable { + fileprivate var _storage: _AtomicInt + + init(startingValue: Int = 0) { + self._storage = .create(startingValue: startingValue) } - - deinit { - storage.deallocate() + + /// The counter's current value. + var value: Int { + _storage.value } + + /// Returns the current value and increments it. + func next() -> Int { + _storage.next() + } +} - /// The current value. +fileprivate final class _AtomicInt: ManagedBuffer { + static func create(startingValue: Int) -> Self { + return super.create(minimumCapacity: 1) { buffer in + buffer.withUnsafeMutablePointerToElements { elements in + elements.initialize(to: startingValue) + } + return () + } as! Self + } + var value: Int { - _swift_stdlib_atomicLoadInt(object: storage) + withUnsafeMutablePointerToElements { + _swift_stdlib_atomicLoadInt(object: $0) + } } - + /// Returns the current value and increments it. func next() -> Int { - _swift_stdlib_atomicFetchAddInt(object: storage, operand: 1) + withUnsafeMutablePointerToElements { + _swift_stdlib_atomicFetchAddInt(object: $0, operand: 1) + } } }