Skip to content

Commit 2a65025

Browse files
authored
Merge pull request swiftlang#857 from CodaFi/a-critique-of-pure-reason
SyntaxArena: thread safety part 2
2 parents 9e2443e + 3dd7635 commit 2a65025

File tree

9 files changed

+203
-31
lines changed

9 files changed

+203
-31
lines changed

Sources/SwiftParser/Parser.swift

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,10 @@ extension Parser {
4242
// Extended lifetime is required because `SyntaxArena` in the parser must
4343
// be alive until `Syntax(raw:)` retains the arena.
4444
return withExtendedLifetime(parser) {
45-
let rawSourceFile = parser.parseSourceFile()
46-
return rawSourceFile.syntax
45+
parser.arena.assumingSingleThread {
46+
let rawSourceFile = parser.parseSourceFile()
47+
return rawSourceFile.syntax
48+
}
4749
}
4850
}
4951
}
@@ -120,7 +122,8 @@ extension Parser {
120122
/// tokens as needed to disambiguate a parse. However, because lookahead
121123
/// operates on a copy of the lexical stream, no input tokens are lost..
122124
public struct Parser: TokenConsumer {
123-
let arena: SyntaxArena
125+
@_spi(RawSyntax)
126+
public var arena: SyntaxArena
124127
/// A view of the sequence of lexemes in the input.
125128
var lexemes: Lexer.LexemeSequence
126129
/// The current token. If there was no input, this token will have a kind of `.eof`.

Sources/SwiftSyntax/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ add_library(SwiftSyntax STATIC
1010
AbsolutePosition.swift
1111
BumpPtrAllocator.swift
1212
IncrementalParseTransition.swift
13+
PlatformMutex.swift
1314
SourceLength.swift
1415
SourceLocation.swift
1516
SourcePresence.swift
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
//===------ PlatformMutex.swift - Platform-specific Mutual Exclusion -----===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2022 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
#if canImport(Darwin)
14+
@_implementationOnly import Darwin
15+
#elseif canImport(Glibc)
16+
@_implementationOnly import Glibc
17+
#elseif canImport(WinSDK)
18+
@_implementationOnly import WinSDK
19+
#endif
20+
21+
/// A protocol that platform-specific mutual exclusion primitives should conform to.
22+
final class PlatformMutex {
23+
// FIXME: Use os_unfair_lock when we bump to macOS 12.0 on Darwin
24+
#if canImport(Darwin) || canImport(Glibc)
25+
typealias Primitive = pthread_mutex_t
26+
#elseif canImport(WinSDK)
27+
typealias Primitive = SRWLOCK
28+
#endif
29+
typealias PlatformLock = UnsafeMutablePointer<Primitive>
30+
31+
private let lock: PlatformLock
32+
33+
/// Allocate memory for, and initialize, the mutex in a platform-specific fashion.
34+
///
35+
/// - Parameter allocator: An allocator
36+
init(allocator: BumpPtrAllocator) {
37+
let storage = allocator.allocate(Primitive.self, count: 1).baseAddress!
38+
storage.initialize(to: Primitive())
39+
self.lock = storage
40+
Self.initialize(self.lock)
41+
}
42+
43+
/// Deinitialize the memory associated with the mutex.
44+
///
45+
/// - Warning: Do not call `.deallocate()` on any pointers allocated by the
46+
/// `BumpPtrAllocator` here.
47+
func deinitialize() {
48+
#if canImport(Darwin) || canImport(Glibc)
49+
pthread_mutex_destroy(self.lock)
50+
#endif
51+
self.lock.deinitialize(count: 1)
52+
}
53+
54+
/// Invoke the provided block under the protection of the mutex.
55+
func withGuard<T>(body: () throws -> T) rethrows -> T {
56+
Self.lock(self.lock)
57+
defer { Self.unlock(self.lock) }
58+
return try body()
59+
}
60+
}
61+
62+
extension PlatformMutex {
63+
private static func initialize(_ platformLock: PlatformLock) {
64+
#if canImport(Darwin)
65+
// HACK: On Darwin, the value of PTHREAD_MUTEX_INITIALIZER installs
66+
// signature bits into the mutex that are later checked by other aspects
67+
// of the mutex API. This is a completely optional POSIX-ism that most
68+
// Linuxes don't implement - often so that (global) lock variables can be
69+
// stuck in .bss. Swift doesn't know how to import
70+
// PTHREAD_MUTEX_INITIALIZER, so we'll replicate its signature-installing
71+
// magic with the bit it can import.
72+
platformLock.pointee.__sig = Int(_PTHREAD_MUTEX_SIG_init)
73+
let result = pthread_mutex_init(platformLock, nil)
74+
precondition(result == 0)
75+
#elseif canImport(Glibc)
76+
let result = pthread_mutex_init(platformLock, nil)
77+
precondition(result == 0)
78+
#elseif canImport(WinSDK)
79+
InitializeSRWLock(platformLock)
80+
#endif
81+
}
82+
83+
private static func lock(_ platformLock: PlatformLock) {
84+
#if canImport(Darwin) || canImport(Glibc)
85+
let result = pthread_mutex_lock(platformLock)
86+
assert(result == 0)
87+
#elseif canImport(WinSDK)
88+
AcquireSRWLockExclusive(platformLock)
89+
#endif
90+
}
91+
92+
private static func unlock(_ platformLock: PlatformLock) {
93+
#if canImport(Darwin) || canImport(Glibc)
94+
let result = pthread_mutex_unlock(platformLock)
95+
assert(result == 0)
96+
#elseif canImport(WinSDK)
97+
ReleaseSRWLockExclusive(platformLock)
98+
#endif
99+
}
100+
}

Sources/SwiftSyntax/SyntaxArena.swift

Lines changed: 63 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -28,28 +28,57 @@ public class SyntaxArena {
2828
private var hasParent: Bool
2929
private var parseTriviaFunction: ParseTriviaFunction
3030

31+
/// Thread safe guard.
32+
private let lock: PlatformMutex
33+
private var singleThreadMode: Bool
34+
3135
@_spi(RawSyntax)
3236
public init(parseTriviaFunction: @escaping ParseTriviaFunction) {
33-
allocator = BumpPtrAllocator()
34-
children = []
35-
sourceBuffer = .init(start: nil, count: 0)
36-
hasParent = false
37+
self.allocator = BumpPtrAllocator()
38+
self.lock = PlatformMutex(allocator: self.allocator)
39+
self.singleThreadMode = false
40+
self.children = []
41+
self.sourceBuffer = .init(start: nil, count: 0)
42+
self.hasParent = false
3743
self.parseTriviaFunction = parseTriviaFunction
3844
}
3945

46+
deinit {
47+
// Make sure we give the platform lock a chance to deinitialize any
48+
// memory it used.
49+
lock.deinitialize()
50+
}
51+
4052
public convenience init() {
4153
self.init(parseTriviaFunction: _defaultParseTriviaFunction(_:_:))
4254
}
4355

56+
private func withGuard<R>(_ body: () throws -> R) rethrows -> R {
57+
if self.singleThreadMode {
58+
return try body()
59+
} else {
60+
return try self.lock.withGuard(body: body)
61+
}
62+
}
63+
64+
public func assumingSingleThread<R>(body: () throws -> R) rethrows -> R {
65+
let oldValue = self.singleThreadMode
66+
defer { self.singleThreadMode = oldValue }
67+
self.singleThreadMode = true
68+
return try body()
69+
}
70+
4471
/// Copies a source buffer in to the memory this arena manages, and returns
4572
/// the interned buffer.
4673
///
4774
/// The interned buffer is guaranteed to be null-terminated.
4875
/// `contains(address _:)` is faster if the address is inside the memory
4976
/// range this function returned.
5077
public func internSourceBuffer(_ buffer: UnsafeBufferPointer<UInt8>) -> UnsafeBufferPointer<UInt8> {
78+
let allocated = self.withGuard {
79+
allocator.allocate(UInt8.self, count: buffer.count + /* for NULL */1)
80+
}
5181
precondition(sourceBuffer.baseAddress == nil, "SourceBuffer should only be set once.")
52-
let allocated = allocator.allocate(UInt8.self, count: buffer.count + /* for NULL */1)
5382
_ = allocated.initialize(from: buffer)
5483

5584
// NULL terminate.
@@ -69,20 +98,27 @@ public class SyntaxArena {
6998
/// Allocates a buffer of `RawSyntax?` with the given count, then returns the
7099
/// uninitlialized memory range as a `UnsafeMutableBufferPointer<RawSyntax?>`.
71100
func allocateRawSyntaxBuffer(count: Int) -> UnsafeMutableBufferPointer<RawSyntax?> {
72-
return allocator.allocate(RawSyntax?.self, count: count)
101+
return self.withGuard {
102+
allocator.allocate(RawSyntax?.self, count: count)
103+
}
73104
}
74105

75106
/// Allcates a buffer of `RawTriviaPiece` with the given count, then returns
76107
/// the uninitialized memory range as a `UnsafeMutableBufferPointer<RawTriviaPiece>`.
77108
func allocateRawTriviaPieceBuffer(
78-
count: Int) -> UnsafeMutableBufferPointer<RawTriviaPiece> {
79-
return allocator.allocate(RawTriviaPiece.self, count: count)
109+
count: Int
110+
) -> UnsafeMutableBufferPointer<RawTriviaPiece> {
111+
return self.withGuard {
112+
allocator.allocate(RawTriviaPiece.self, count: count)
80113
}
114+
}
81115

82116
/// Allcates a buffer of `UInt8` with the given count, then returns the
83117
/// uninitialized memory range as a `UnsafeMutableBufferPointer<UInt8>`.
84118
func allocateTextBuffer(count: Int) -> UnsafeMutableBufferPointer<UInt8> {
85-
return allocator.allocate(UInt8.self, count: count)
119+
return self.withGuard {
120+
allocator.allocate(UInt8.self, count: count)
121+
}
86122
}
87123

88124
/// Copies the contents of a `SyntaxText` to the memory this arena manages,
@@ -114,7 +150,9 @@ public class SyntaxArena {
114150
/// Copies a `RawSyntaxData` to the memory this arena manages, and retuns the
115151
/// pointer to the destination.
116152
func intern(_ value: RawSyntaxData) -> UnsafePointer<RawSyntaxData> {
117-
let allocated = allocator.allocate(RawSyntaxData.self, count: 1).baseAddress!
153+
let allocated = self.withGuard {
154+
allocator.allocate(RawSyntaxData.self, count: 1).baseAddress!
155+
}
118156
allocated.initialize(to: value)
119157
return UnsafePointer(allocated)
120158
}
@@ -128,21 +166,26 @@ public class SyntaxArena {
128166
/// See also `RawSyntax.layout()`.
129167
func addChild(_ arenaRef: SyntaxArenaRef) {
130168
if SyntaxArenaRef(self) == arenaRef { return }
131-
132169
let other = arenaRef.value
133170

134-
precondition(
135-
!self.hasParent,
136-
"an arena can't have a new child once it's owned by other arenas")
137-
138-
other.hasParent = true
139-
children.insert(other)
171+
other.withGuard {
172+
self.withGuard {
173+
precondition(
174+
!self.hasParent,
175+
"an arena can't have a new child once it's owned by other arenas")
176+
177+
other.hasParent = true
178+
children.insert(other)
179+
}
180+
}
140181
}
141182

142183
/// Recursively checks if this arena contains given `arena` as a descendant.
143184
func contains(arena: SyntaxArena) -> Bool {
144-
return children.contains { child in
145-
child === arena || child.contains(arena: arena)
185+
self.withGuard {
186+
children.contains { child in
187+
child === arena || child.contains(arena: arena)
188+
}
146189
}
147190
}
148191

@@ -154,7 +197,7 @@ public class SyntaxArena {
154197
public func contains(text: SyntaxText) -> Bool {
155198
return (text.isEmpty ||
156199
sourceBufferContains(text.baseAddress!) ||
157-
allocator.contains(address: text.baseAddress!))
200+
self.withGuard({allocator.contains(address: text.baseAddress!)}))
158201
}
159202

160203
@_spi(RawSyntax)

Sources/SwiftSyntax/SyntaxText.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@
1111
//===----------------------------------------------------------------------===//
1212

1313
#if canImport(Darwin)
14-
import Darwin
14+
@_implementationOnly import Darwin
1515
#elseif canImport(Glibc)
16-
import Glibc
16+
@_implementationOnly import Glibc
1717
#endif
1818

1919
/// Represent a string.

Sources/SwiftSyntaxBuilder/Syntax+StringInterpolation.swift

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
@_spi(RawSyntax)
2-
import SwiftSyntax
3-
import SwiftParser
1+
@_spi(RawSyntax) import SwiftSyntax
2+
@_spi(RawSyntax) import SwiftParser
43

54
/// An individual interpolated syntax node.
65
struct InterpolatedSyntaxNode {
@@ -101,7 +100,9 @@ extension SyntaxExpressibleByStringInterpolation {
101100
var parser = Parser(buffer)
102101
// FIXME: When the parser supports incremental parsing, put the
103102
// interpolatedSyntaxNodes in so we don't have to parse them again.
104-
return Self.parse(from: &parser)
103+
return parser.arena.assumingSingleThread {
104+
return Self.parse(from: &parser)
105+
}
105106
}
106107
}
107108

Tests/SwiftParserTest/Linkage.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ final class LinkageTest: XCTestCase {
3232
.library("-lswiftCompatibility56", condition: .mayBeAbsent("Starting in Xcode 14 this library is not always autolinked")),
3333
.library("-lswiftCompatibilityConcurrency"),
3434
.library("-lswiftCore"),
35-
.library("-lswiftDarwin"),
3635
.library("-lswiftSwiftOnoneSupport", condition: .when(configuration: .debug)),
3736
.library("-lswift_Concurrency"),
3837
.library("-lswift_StringProcessing", condition: .mayBeAbsent("Starting in Xcode 14 this library is not always autolinked")),

Tests/SwiftSyntaxTest/MultithreadingTests.swift

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import XCTest
2-
import SwiftSyntax
2+
@_spi(RawSyntax) import SwiftSyntax
3+
34

45
public class MultithreadingTests: XCTestCase {
56

@@ -15,6 +16,29 @@ public class MultithreadingTests: XCTestCase {
1516
}
1617
}
1718

19+
public func testConcurrentArena() {
20+
let arena = SyntaxArena()
21+
22+
DispatchQueue.concurrentPerform(iterations: 100) { i in
23+
var identStr = " ident\(i) "
24+
let tokenRaw = identStr.withSyntaxText { text in
25+
RawTokenSyntax(
26+
kind: .identifier,
27+
wholeText: arena.intern(text),
28+
textRange: 1..<(text.count-1),
29+
presence: .present,
30+
arena: arena)
31+
}
32+
let identifierExprRaw = RawIdentifierExprSyntax(
33+
identifier: tokenRaw,
34+
declNameArguments: nil,
35+
arena: arena)
36+
37+
let expr = Syntax(raw: RawSyntax(identifierExprRaw)).as(IdentifierExprSyntax.self)!
38+
XCTAssertEqual(expr.identifier.text, "ident\(i)")
39+
}
40+
}
41+
1842
public func testTwoAccesses() {
1943
let tuple = TupleTypeSyntax(
2044
leftParen: .leftParenToken(),

utils/group.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,5 +56,6 @@
5656
"SyntaxArena.swift",
5757
"SyntaxVerifier.swift",
5858
"BumpPtrAllocator.swift",
59+
"PlatformMutex.swift",
5960
]
6061
}

0 commit comments

Comments
 (0)