Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions Sources/_StringProcessing/Regex/DSLTree.swift
Original file line number Diff line number Diff line change
Expand Up @@ -416,16 +416,16 @@ 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 {
base
}

public init() {
base = Self.counter
Self.counter += 1
base = Self.counter.next()
}

init(_ base: Int) {
Expand Down
57 changes: 57 additions & 0 deletions Sources/_StringProcessing/Utility/AtomicCounter.swift
Original file line number Diff line number Diff line change
@@ -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<Void, Int> {
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)
}
}
}
144 changes: 144 additions & 0 deletions Tests/RegexBuilderTests/ReferenceConcurrencyTests.swift
Original file line number Diff line number Diff line change
@@ -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<Int>.allocate(capacity: total)
defer {
ids.deallocate()
}

DispatchQueue.concurrentPerform(iterations: Self.threadCount) { thread in
let base = thread * perThread
for i in 0..<perThread {
ids[base + i] = counter.next()
}
}

let unique = Set(UnsafeBufferPointer(start: ids, count: total))
XCTAssertEqual(
unique.count, total,
"\(total - unique.count) of \(total) ints were duplicates")
}

func testConcurrentReferenceIDsAreUnique() {
let perThread = 2_000
let total = Self.threadCount * perThread

nonisolated(unsafe)
let refs = UnsafeMutablePointer<Reference<Substring>>.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..<perThread {
refs[base + i] = Reference()
}
}

let unique = Set(UnsafeBufferPointer(start: refs, count: total).map { $0._raw })
XCTAssertEqual(
unique.count, total,
"\(total - unique.count) of \(total) references were duplicates")
}
}

#endif

extension ReferenceConcurrencyTests {
func testConcurrentMatching() async {
func parseOnce(_ input: String) -> 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..<iterations {
group.addTask {
parseOnce("2601:246:4300:63a0:3043:575d:d702:4d4a")
}
}
return await group.compact()
}
XCTAssert(failures.isEmpty)
if !failures.isEmpty {
for f in failures.prefix(10) { print(f) }
}
}
}

extension AsyncSequence {
func compact<T>() async rethrows -> [T] where Element == T? {
try await reduce(into: []) { result, el in if let el { result.append(el) } }
}
}
Loading