Skip to content

Commit 6ba87a8

Browse files
authored
Merge pull request #807 from rintaro/arena-lock
SyntaxArena: thread safety
2 parents 13b2b57 + 322559c commit 6ba87a8

File tree

4 files changed

+148
-21
lines changed

4 files changed

+148
-21
lines changed

Sources/SwiftParser/Parser.swift

Lines changed: 4 additions & 2 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 Syntax(raw: rawSourceFile.raw).as(SourceFileSyntax.self)!
45+
parser.arena.assumingSingleThread {
46+
let rawSourceFile = parser.parseSourceFile()
47+
return Syntax(raw: rawSourceFile.raw).as(SourceFileSyntax.self)!
48+
}
4749
}
4850
}
4951
}

Sources/SwiftParser/Syntax+StringInterpolation.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,9 @@ extension SyntaxExpressibleByStringInterpolation {
7373
var parser = Parser(buffer)
7474
// FIXME: When the parser supports incremental parsing, put the
7575
// interpolatedSyntaxNodes in so we don't have to parse them again.
76-
return Self.parse(from: &parser)
76+
return parser.arena.assumingSingleThread {
77+
return Self.parse(from: &parser)
78+
}
7779
}
7880
}
7981

Sources/SwiftSyntax/SyntaxArena.swift

Lines changed: 116 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,69 @@
1010
//
1111
//===----------------------------------------------------------------------===//
1212

13+
#if canImport(Darwin)
14+
import Darwin
15+
16+
struct ScopeGuard {
17+
private let lock: os_unfair_lock_t
18+
init(allocator: BumpPtrAllocator) {
19+
let storage = allocator.allocate(os_unfair_lock.self, count: 1).baseAddress!
20+
storage.initialize(to: os_unfair_lock())
21+
self.lock = os_unfair_lock_t(storage)
22+
}
23+
24+
func deinitialize() {}
25+
26+
func withGuard<T>(body: () throws -> T) rethrows -> T {
27+
os_unfair_lock_lock(lock)
28+
defer { os_unfair_lock_unlock(lock)}
29+
return try body()
30+
}
31+
}
32+
33+
#elseif canImport(Glibc)
34+
import Glibc
35+
36+
struct ScopeGuard {
37+
private let lock: UnsafeMutablePointer<pthread_mutex_t>
38+
init(allocator: BumpPtrAllocator) {
39+
let storage = allocator.allocate(pthread_mutex_t.self, count: 1).baseAddress!
40+
storage.initialize(to: pthread_mutex_t())
41+
pthread_mutex_init(storage, nil)
42+
self.lock = storage
43+
}
44+
func deinitialize() {
45+
pthread_mutex_destroy(self.lock)
46+
}
47+
func withGuard<T>(body: () throws -> T) rethrows -> T {
48+
pthread_mutex_lock(self.lock)
49+
defer { pthread_mutex_unlock(self.lock) }
50+
return try body()
51+
}
52+
}
53+
54+
#else
55+
// FIXME: Support other platforms.
56+
57+
/// Dummy mutex that doesn't actually guard at all.
58+
class ScopeGuard {
59+
init() {}
60+
func deinitialize() {}
61+
func withGuard<T>(body: () throws -> T) rethrows -> T {
62+
return try body()
63+
}
64+
}
65+
#endif
66+
1367
public class SyntaxArena {
1468

1569
@_spi(RawSyntax)
1670
public typealias ParseTriviaFunction = (_ source: SyntaxText, _ position: TriviaPosition) -> [RawTriviaPiece]
1771

72+
/// Thread safe guard.
73+
private let lock: ScopeGuard
74+
private var singleThreadMode: Bool
75+
1876
/// Bump-pointer allocator for all "intern" methods.
1977
private let allocator: BumpPtrAllocator
2078
/// Source file buffer the Syntax tree represents.
@@ -30,26 +88,53 @@ public class SyntaxArena {
3088

3189
@_spi(RawSyntax)
3290
public init(parseTriviaFunction: @escaping ParseTriviaFunction) {
33-
allocator = BumpPtrAllocator()
91+
let allocator = BumpPtrAllocator()
92+
self.lock = ScopeGuard(allocator: allocator)
93+
self.singleThreadMode = false
94+
self.allocator = allocator
3495
children = []
3596
sourceBuffer = .init(start: nil, count: 0)
3697
hasParent = false
3798
self.parseTriviaFunction = parseTriviaFunction
3899
}
39100

101+
deinit {
102+
// NOTE: We don't make `ScopeGuard` a class and `deinit` in it to
103+
// deinitialize it because the actual lock value is in `allocator`, and we
104+
// want to make sure to deinitialize the lock before destroying the allocator.
105+
lock.deinitialize()
106+
}
107+
40108
public convenience init() {
41109
self.init(parseTriviaFunction: _defaultParseTriviaFunction(_:_:))
42110
}
43111

112+
private func withGuard<R>(_ body: () throws -> R) rethrows -> R {
113+
if self.singleThreadMode {
114+
return try body()
115+
} else {
116+
return try self.lock.withGuard(body: body)
117+
}
118+
}
119+
120+
public func assumingSingleThread<R>(body: () throws -> R) rethrows -> R {
121+
let oldValue = self.singleThreadMode
122+
defer { self.singleThreadMode = oldValue }
123+
self.singleThreadMode = true
124+
return try body()
125+
}
126+
44127
/// Copies a source buffer in to the memory this arena manages, and returns
45128
/// the interned buffer.
46129
///
47130
/// The interned buffer is guaranteed to be null-terminated.
48131
/// `contains(address _:)` is faster if the address is inside the memory
49132
/// range this function returned.
50133
public func internSourceBuffer(_ buffer: UnsafeBufferPointer<UInt8>) -> UnsafeBufferPointer<UInt8> {
134+
let allocated = self.withGuard {
135+
allocator.allocate(UInt8.self, count: buffer.count + /* for NULL */1)
136+
}
51137
precondition(sourceBuffer.baseAddress == nil, "SourceBuffer should only be set once.")
52-
let allocated = allocator.allocate(UInt8.self, count: buffer.count + /* for NULL */1)
53138
_ = allocated.initialize(from: buffer)
54139

55140
// NULL terminate.
@@ -69,20 +154,27 @@ public class SyntaxArena {
69154
/// Allocates a buffer of `RawSyntax?` with the given count, then returns the
70155
/// uninitlialized memory range as a `UnsafeMutableBufferPointer<RawSyntax?>`.
71156
func allocateRawSyntaxBuffer(count: Int) -> UnsafeMutableBufferPointer<RawSyntax?> {
72-
return allocator.allocate(RawSyntax?.self, count: count)
157+
return self.withGuard {
158+
allocator.allocate(RawSyntax?.self, count: count)
159+
}
73160
}
74161

75162
/// Allcates a buffer of `RawTriviaPiece` with the given count, then returns
76163
/// the uninitialized memory range as a `UnsafeMutableBufferPointer<RawTriviaPiece>`.
77164
func allocateRawTriviaPieceBuffer(
78-
count: Int) -> UnsafeMutableBufferPointer<RawTriviaPiece> {
79-
return allocator.allocate(RawTriviaPiece.self, count: count)
165+
count: Int
166+
) -> UnsafeMutableBufferPointer<RawTriviaPiece> {
167+
return self.withGuard {
168+
allocator.allocate(RawTriviaPiece.self, count: count)
80169
}
170+
}
81171

82172
/// Allcates a buffer of `UInt8` with the given count, then returns the
83173
/// uninitialized memory range as a `UnsafeMutableBufferPointer<UInt8>`.
84174
func allocateTextBuffer(count: Int) -> UnsafeMutableBufferPointer<UInt8> {
85-
return allocator.allocate(UInt8.self, count: count)
175+
return self.withGuard {
176+
allocator.allocate(UInt8.self, count: count)
177+
}
86178
}
87179

88180
/// Copies the contents of a `SyntaxText` to the memory this arena manages,
@@ -114,7 +206,9 @@ public class SyntaxArena {
114206
/// Copies a `RawSyntaxData` to the memory this arena manages, and retuns the
115207
/// pointer to the destination.
116208
func intern(_ value: RawSyntaxData) -> UnsafePointer<RawSyntaxData> {
117-
let allocated = allocator.allocate(RawSyntaxData.self, count: 1).baseAddress!
209+
let allocated = self.withGuard {
210+
allocator.allocate(RawSyntaxData.self, count: 1).baseAddress!
211+
}
118212
allocated.initialize(to: value)
119213
return UnsafePointer(allocated)
120214
}
@@ -128,21 +222,26 @@ public class SyntaxArena {
128222
/// See also `RawSyntax.layout()`.
129223
func addChild(_ arenaRef: SyntaxArenaRef) {
130224
if SyntaxArenaRef(self) == arenaRef { return }
131-
132225
let other = arenaRef.value
133226

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)
227+
other.withGuard {
228+
self.withGuard {
229+
precondition(
230+
!self.hasParent,
231+
"an arena can't have a new child once it's owned by other arenas")
232+
233+
other.hasParent = true
234+
children.insert(other)
235+
}
236+
}
140237
}
141238

142239
/// Recursively checks if this arena contains given `arena` as a descendant.
143240
func contains(arena: SyntaxArena) -> Bool {
144-
return children.contains { child in
145-
child === arena || child.contains(arena: arena)
241+
self.withGuard {
242+
children.contains { child in
243+
child === arena || child.contains(arena: arena)
244+
}
146245
}
147246
}
148247

@@ -154,7 +253,7 @@ public class SyntaxArena {
154253
public func contains(text: SyntaxText) -> Bool {
155254
return (text.isEmpty ||
156255
sourceBufferContains(text.baseAddress!) ||
157-
allocator.contains(address: text.baseAddress!))
256+
self.withGuard({allocator.contains(address: text.baseAddress!)}))
158257
}
159258

160259
@_spi(RawSyntax)

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(),

0 commit comments

Comments
 (0)