diff --git a/Sources/_StringProcessing/Regex/DSLTree.swift b/Sources/_StringProcessing/Regex/DSLTree.swift index 36c4baf29..2449bfe0e 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() + 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..31b34b6a6 --- /dev/null +++ b/Sources/_StringProcessing/Utility/AtomicCounter.swift @@ -0,0 +1,57 @@ +//===----------------------------------------------------------------------===// +// +// 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. +struct AtomicCounter: @unchecked Sendable { + fileprivate var _storage: _AtomicInt + + init(startingValue: Int = 0) { + self._storage = .create(startingValue: startingValue) + } + + /// The counter's current value. + var value: Int { + _storage.value + } + + /// Returns the current value and increments it. + func next() -> Int { + _storage.next() + } +} + +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 { + withUnsafeMutablePointerToElements { + _swift_stdlib_atomicLoadInt(object: $0) + } + } + + /// Returns the current value and increments it. + func next() -> Int { + withUnsafeMutablePointerToElements { + _swift_stdlib_atomicFetchAddInt(object: $0, 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) } } + } +}