Skip to content

Commit acff16c

Browse files
committed
SyntaxArena: thread safety
Use mutex to guard concurrent mutation to the arena.
1 parent e90d261 commit acff16c

File tree

5 files changed

+142
-19
lines changed

5 files changed

+142
-19
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: 97 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,60 @@
1010
//
1111
//===----------------------------------------------------------------------===//
1212

13+
#if canImport(Darwin)
14+
import Darwin
15+
16+
class ScopeGuard {
17+
private var lock: os_unfair_lock
18+
init() {
19+
self.lock = os_unfair_lock()
20+
}
21+
func withGuard<T>(body: () throws -> T) rethrows -> T {
22+
os_unfair_lock_lock(&lock)
23+
defer { os_unfair_lock_unlock(&lock)}
24+
return try body()
25+
}
26+
}
27+
28+
#elseif canImport(Glibc)
29+
import Glibc
30+
31+
class ScopeGuard {
32+
var lock: pthread_mutex_t
33+
init() {
34+
self.lock = pthread_mutex_t()
35+
pthread_mutex_init(&self.lock, nil)
36+
}
37+
deinit {
38+
pthread_mutex_destroy(&self.lock)
39+
}
40+
func withGuard<T>(body: () throws -> T) rethrows -> T {
41+
pthread_mutex_lock(&lock)
42+
defer { pthread_mutex_unlock(&lock) }
43+
return try body()
44+
}
45+
}
46+
#else
47+
// FIXME: Support other platforms.
48+
49+
/// Dummy mutex that doesn't actually guard at all.
50+
class ScopeGuard {
51+
init() {}
52+
func withGuard<T>(body: () throws -> T) rethrows -> T {
53+
return try body()
54+
}
55+
}
56+
#endif
57+
1358
public class SyntaxArena {
1459

1560
@_spi(RawSyntax)
1661
public typealias ParseTriviaFunction = (_ source: SyntaxText, _ position: TriviaPosition) -> [RawTriviaPiece]
1762

63+
/// Thread safe guard.
64+
private let lock: ScopeGuard
65+
private var singleThreadMode: Bool
66+
1867
/// Bump-pointer allocator for all "intern" methods.
1968
private let allocator: BumpPtrAllocator
2069
/// Source file buffer the Syntax tree represents.
@@ -30,6 +79,8 @@ public class SyntaxArena {
3079

3180
@_spi(RawSyntax)
3281
public init(parseTriviaFunction: @escaping ParseTriviaFunction) {
82+
lock = ScopeGuard()
83+
self.singleThreadMode = false
3384
allocator = BumpPtrAllocator()
3485
children = []
3586
sourceBuffer = .init(start: nil, count: 0)
@@ -41,15 +92,32 @@ public class SyntaxArena {
4192
self.init(parseTriviaFunction: _defaultParseTriviaFunction(_:_:))
4293
}
4394

95+
private func withGuard<R>(body: () throws -> R) rethrows -> R {
96+
if self.singleThreadMode {
97+
return try body()
98+
} else {
99+
return try self.lock.withGuard(body: body)
100+
}
101+
}
102+
103+
public func assumingSingleThread<R>(body: () throws -> R) rethrows -> R {
104+
let oldValue = self.singleThreadMode
105+
defer { self.singleThreadMode = oldValue }
106+
self.singleThreadMode = true
107+
return try body()
108+
}
109+
44110
/// Copies a source buffer in to the memory this arena manages, and returns
45111
/// the interned buffer.
46112
///
47113
/// The interned buffer is guaranteed to be null-terminated.
48114
/// `contains(address _:)` is faster if the address is inside the memory
49115
/// range this function returned.
50116
public func internSourceBuffer(_ buffer: UnsafeBufferPointer<UInt8>) -> UnsafeBufferPointer<UInt8> {
117+
let allocated = lock.withGuard {
118+
allocator.allocate(UInt8.self, count: buffer.count + /* for NULL */1)
119+
}
51120
precondition(sourceBuffer.baseAddress == nil, "SourceBuffer should only be set once.")
52-
let allocated = allocator.allocate(UInt8.self, count: buffer.count + /* for NULL */1)
53121
_ = allocated.initialize(from: buffer)
54122

55123
// NULL terminate.
@@ -69,20 +137,27 @@ public class SyntaxArena {
69137
/// Allocates a buffer of `RawSyntax?` with the given count, then returns the
70138
/// uninitlialized memory range as a `UnsafeMutableBufferPointer<RawSyntax?>`.
71139
func allocateRawSyntaxBuffer(count: Int) -> UnsafeMutableBufferPointer<RawSyntax?> {
72-
return allocator.allocate(RawSyntax?.self, count: count)
140+
return self.withGuard {
141+
allocator.allocate(RawSyntax?.self, count: count)
142+
}
73143
}
74144

75145
/// Allcates a buffer of `RawTriviaPiece` with the given count, then returns
76146
/// the uninitialized memory range as a `UnsafeMutableBufferPointer<RawTriviaPiece>`.
77147
func allocateRawTriviaPieceBuffer(
78-
count: Int) -> UnsafeMutableBufferPointer<RawTriviaPiece> {
79-
return allocator.allocate(RawTriviaPiece.self, count: count)
148+
count: Int
149+
) -> UnsafeMutableBufferPointer<RawTriviaPiece> {
150+
return self.withGuard {
151+
allocator.allocate(RawTriviaPiece.self, count: count)
80152
}
153+
}
81154

82155
/// Allcates a buffer of `UInt8` with the given count, then returns the
83156
/// uninitialized memory range as a `UnsafeMutableBufferPointer<UInt8>`.
84157
func allocateTextBuffer(count: Int) -> UnsafeMutableBufferPointer<UInt8> {
85-
return allocator.allocate(UInt8.self, count: count)
158+
return self.withGuard {
159+
allocator.allocate(UInt8.self, count: count)
160+
}
86161
}
87162

88163
/// Copies the contents of a `SyntaxText` to the memory this arena manages,
@@ -114,7 +189,9 @@ public class SyntaxArena {
114189
/// Copies a `RawSyntaxData` to the memory this arena manages, and retuns the
115190
/// pointer to the destination.
116191
func intern(_ value: RawSyntaxData) -> UnsafePointer<RawSyntaxData> {
117-
let allocated = allocator.allocate(RawSyntaxData.self, count: 1).baseAddress!
192+
let allocated = lock.withGuard {
193+
allocator.allocate(RawSyntaxData.self, count: 1).baseAddress!
194+
}
118195
allocated.initialize(to: value)
119196
return UnsafePointer(allocated)
120197
}
@@ -128,21 +205,26 @@ public class SyntaxArena {
128205
/// See also `RawSyntax.layout()`.
129206
func addChild(_ arenaRef: SyntaxArenaRef) {
130207
if SyntaxArenaRef(self) == arenaRef { return }
131-
132208
let other = arenaRef.value
133209

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)
210+
other.withGuard {
211+
self.withGuard {
212+
precondition(
213+
!self.hasParent,
214+
"an arena can't have a new child once it's owned by other arenas")
215+
216+
other.hasParent = true
217+
children.insert(other)
218+
}
219+
}
140220
}
141221

142222
/// Recursively checks if this arena contains given `arena` as a descendant.
143223
func contains(arena: SyntaxArena) -> Bool {
144-
return children.contains { child in
145-
child === arena || child.contains(arena: arena)
224+
self.withGuard {
225+
children.contains { child in
226+
child === arena || child.contains(arena: arena)
227+
}
146228
}
147229
}
148230

Tests/SwiftParserTest/ParserTests.swift

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,24 @@ public class ParserTests: XCTestCase {
7474
runParserTests(
7575
name: "Self-parse tests", path: currentDir, checkDiagnostics: true
7676
)
77+
78+
let fileURLs = FileManager.default
79+
.enumerator(at: currentDir, includingPropertiesForKeys: nil)!
80+
.compactMap({ $0 as? URL })
81+
.filter {
82+
$0.pathExtension == "swift"
83+
}
84+
85+
measure {
86+
for fileURL in fileURLs {
87+
let source = try! Data(contentsOf: fileURL)
88+
for _ in 0 ..< 10 {
89+
source.withUnsafeBytes { buf in
90+
_ = try! Parser.parse(source: buf.bindMemory(to: UInt8.self))
91+
}
92+
}
93+
}
94+
}
7795
}
7896

7997
func testSwiftTestsuite() throws {

Tests/SwiftSyntaxTest/MultithreadingTests.swift

Lines changed: 20 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,24 @@ 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 token = Syntax(raw: RawSyntax(tokenRaw)).as(TokenSyntax.self)!
33+
XCTAssertEqual(token.text, "ident\(i)")
34+
}
35+
}
36+
1837
public func testTwoAccesses() {
1938
let tuple = TupleTypeSyntax(
2039
leftParen: .leftParenToken(),

0 commit comments

Comments
 (0)