Skip to content

Commit b6898d1

Browse files
committed
[SyntaxRewriter] Introduce SyntaxNodeFactory
Factor out 'Syntax.Info' reusing into 'SyntaxNodeFactory' and 'SyntaxInfoRepository' as the backing storage. The underlying storage now uses a fixed length 'ManagedBuffer' to avoid 'ContiguousArray' overhead.
1 parent 6be8e8d commit b6898d1

File tree

4 files changed

+96
-68
lines changed

4 files changed

+96
-68
lines changed

CodeGeneration/Sources/generate-swift-syntax/templates/swiftsyntax/SyntaxRewriterFile.swift

Lines changed: 5 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -44,19 +44,8 @@ let syntaxRewriterFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
4444

4545
DeclSyntax(
4646
"""
47-
/// `Syntax.Info` salvaged from the node being deinitialized in 'visitChildren'.
48-
///
49-
/// Instead of deallocating them and allocating memory for new syntax nodes, store the allocated memory in an array.
50-
/// We can then re-use them to create new syntax nodes.
51-
///
52-
/// The array's size should be a typical nesting depth of a Swift file. That way we can store all allocated syntax
53-
/// nodes when unwinding the visitation stack.
54-
///
55-
/// The actual `info` stored in the `Syntax.Info` objects is garbage. It needs to be set when any of the `Syntax.Info`
56-
/// objects get re-used.
57-
///
58-
/// Note: making the element non-nil causes 'swift::runtime::SwiftTLSContext::get()' traffic somehow.
59-
private var recyclableNodeInfos: ContiguousArray<Syntax.Info?>
47+
/// 'Syntax' object factory recycling 'Syntax.Info' instances.
48+
private let nodeFactory: SyntaxNodeFactory = SyntaxNodeFactory()
6049
"""
6150
)
6251

@@ -65,8 +54,6 @@ let syntaxRewriterFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
6554
public init(viewMode: SyntaxTreeViewMode = .sourceAccurate) {
6655
self.viewMode = viewMode
6756
self.arena = nil
68-
self.recyclableNodeInfos = []
69-
self.recyclableNodeInfos.reserveCapacity(64)
7057
}
7158
"""
7259
)
@@ -77,8 +64,6 @@ let syntaxRewriterFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
7764
public init(viewMode: SyntaxTreeViewMode = .sourceAccurate, arena: SyntaxArena? = nil) {
7865
self.viewMode = viewMode
7966
self.arena = arena
80-
self.recyclableNodeInfos = []
81-
self.recyclableNodeInfos.reserveCapacity(64)
8267
}
8368
"""
8469
)
@@ -353,15 +338,7 @@ let syntaxRewriterFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
353338
}
354339
355340
// Build the Syntax node to rewrite
356-
var childNode: Syntax
357-
if !recyclableNodeInfos.isEmpty {
358-
let recycledInfo: Syntax.Info = recyclableNodeInfos.removeLast()!
359-
recycledInfo.info = .nonRoot(.init(parent: Syntax(node), absoluteInfo: info))
360-
childNode = Syntax(child, info: recycledInfo)
361-
} else {
362-
let absoluteRaw = AbsoluteRawSyntax(raw: child, info: info)
363-
childNode = Syntax(absoluteRaw, parent: syntaxNode)
364-
}
341+
var childNode = nodeFactory.create(parent: syntaxNode, raw: child, absoluteInfo: info)
365342
366343
dispatchVisit(&childNode)
367344
if childNode.raw.id != child.id {
@@ -392,14 +369,8 @@ let syntaxRewriterFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
392369
}
393370
}
394371
395-
if recyclableNodeInfos.capacity > recyclableNodeInfos.count,
396-
isKnownUniquelyReferenced(&childNode.info) {
397-
var info: Syntax.Info! = nil
398-
// 'swap' to avoid ref-counting traffic.
399-
swap(&childNode.info, &info)
400-
info.info = nil
401-
recyclableNodeInfos.append(info)
402-
}
372+
// Recycle 'childNode.info'
373+
nodeFactory.dispose(&childNode)
403374
}
404375
405376
if let newLayout {

Sources/SwiftSyntax/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ add_swift_syntax_library(SwiftSyntax
3030
SyntaxCollection.swift
3131
SyntaxHashable.swift
3232
SyntaxIdentifier.swift
33+
SyntaxNodeFactory.swift
3334
SyntaxNodeStructure.swift
3435
SyntaxProtocol.swift
3536
SyntaxText.swift
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2024 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+
/// Reusable 'Syntax.Info' storage.
14+
private struct SyntaxInfoRepository {
15+
private final class _Buffer: ManagedBuffer<Int, Syntax.Info?> {
16+
deinit {
17+
self.withUnsafeMutablePointers { headerPtr, elementPtr in
18+
_ = elementPtr.deinitialize(count: headerPtr.pointee)
19+
}
20+
}
21+
}
22+
23+
/// Fixed capacity which is enough for most use cases.
24+
static var capacity: Int { 64 }
25+
26+
private let buffer: _Buffer
27+
28+
init() {
29+
let buffer = _Buffer.create(minimumCapacity: Self.capacity, makingHeaderWith: { _ in 0 })
30+
self.buffer = buffer as! _Buffer
31+
}
32+
33+
/// Take the 'Syntax.Info' object from the address.
34+
func push(_ info: UnsafeMutablePointer<Syntax.Info?>) {
35+
buffer.withUnsafeMutablePointers { headerPtr, elementPtr in
36+
guard headerPtr.pointee < Self.capacity else {
37+
return
38+
}
39+
assert(info.pointee != nil, "tried to push 'nil' info")
40+
elementPtr.advanced(by: headerPtr.pointee).moveInitialize(from: info, count: 1)
41+
info.initialize(to: nil)
42+
headerPtr.pointee += 1
43+
}
44+
}
45+
46+
/// Vend a 'Swift.Info' object if available.
47+
func pop() -> Syntax.Info? {
48+
return buffer.withUnsafeMutablePointers { headerPtr, elementPtr in
49+
guard headerPtr.pointee > 0 else {
50+
return nil
51+
}
52+
headerPtr.pointee -= 1
53+
return elementPtr.advanced(by: headerPtr.pointee).move()
54+
}
55+
}
56+
}
57+
58+
/// 'Syntax' object factory. This may hold some stocks of recycled 'Syntax.Info'.
59+
struct SyntaxNodeFactory {
60+
private let syntaxInfoRepo: SyntaxInfoRepository = SyntaxInfoRepository()
61+
62+
/// Create a 'Syntax' instance using the supplied info.
63+
///
64+
/// If this factory has a recycled 'Syntax.Info', use one of them. Otherwise, just create a instance by allocating a new one.
65+
@inline(__always)
66+
func create(parent: Syntax, raw: RawSyntax, absoluteInfo: AbsoluteSyntaxInfo) -> Syntax {
67+
if let info = syntaxInfoRepo.pop() {
68+
info.info = .nonRoot(.init(parent: parent, absoluteInfo: absoluteInfo))
69+
return Syntax(raw, info: info)
70+
} else {
71+
return Syntax(raw, parent: parent, absoluteInfo: absoluteInfo)
72+
}
73+
}
74+
75+
/// Dispose a 'Syntax' object.
76+
///
77+
/// 'node.info' is collected for future reuse. 'node' is not usable after calling this.
78+
@inline(__always)
79+
func dispose(_ node: inout Syntax) {
80+
if isKnownUniquelyReferenced(&node.info) {
81+
node.info.unsafelyUnwrapped.info = nil
82+
syntaxInfoRepo.push(&node.info)
83+
}
84+
}
85+
}

Sources/SwiftSyntax/generated/SyntaxRewriter.swift

Lines changed: 5 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -31,33 +31,18 @@ open class SyntaxRewriter {
3131
/// intermediate nodes should be allocated.
3232
private let arena: SyntaxArena?
3333

34-
/// `Syntax.Info` salvaged from the node being deinitialized in 'visitChildren'.
35-
///
36-
/// Instead of deallocating them and allocating memory for new syntax nodes, store the allocated memory in an array.
37-
/// We can then re-use them to create new syntax nodes.
38-
///
39-
/// The array's size should be a typical nesting depth of a Swift file. That way we can store all allocated syntax
40-
/// nodes when unwinding the visitation stack.
41-
///
42-
/// The actual `info` stored in the `Syntax.Info` objects is garbage. It needs to be set when any of the `Syntax.Info`
43-
/// objects get re-used.
44-
///
45-
/// Note: making the element non-nil causes 'swift::runtime::SwiftTLSContext::get()' traffic somehow.
46-
private var recyclableNodeInfos: ContiguousArray<Syntax.Info?>
34+
/// 'Syntax' object factory recycling 'Syntax.Info' instances.
35+
private let nodeFactory: SyntaxNodeFactory = SyntaxNodeFactory()
4736

4837
public init(viewMode: SyntaxTreeViewMode = .sourceAccurate) {
4938
self.viewMode = viewMode
5039
self.arena = nil
51-
self.recyclableNodeInfos = []
52-
self.recyclableNodeInfos.reserveCapacity(64)
5340
}
5441

5542
@_spi(RawSyntax)
5643
public init(viewMode: SyntaxTreeViewMode = .sourceAccurate, arena: SyntaxArena? = nil) {
5744
self.viewMode = viewMode
5845
self.arena = arena
59-
self.recyclableNodeInfos = []
60-
self.recyclableNodeInfos.reserveCapacity(64)
6146
}
6247

6348
/// Rewrite `node`, keeping its parent unless `detach` is `true`.
@@ -3952,15 +3937,7 @@ open class SyntaxRewriter {
39523937
}
39533938

39543939
// Build the Syntax node to rewrite
3955-
var childNode: Syntax
3956-
if !recyclableNodeInfos.isEmpty {
3957-
let recycledInfo: Syntax.Info = recyclableNodeInfos.removeLast()!
3958-
recycledInfo.info = .nonRoot(.init(parent: Syntax(node), absoluteInfo: info))
3959-
childNode = Syntax(child, info: recycledInfo)
3960-
} else {
3961-
let absoluteRaw = AbsoluteRawSyntax(raw: child, info: info)
3962-
childNode = Syntax(absoluteRaw, parent: syntaxNode)
3963-
}
3940+
var childNode = nodeFactory.create(parent: syntaxNode, raw: child, absoluteInfo: info)
39643941

39653942
dispatchVisit(&childNode)
39663943
if childNode.raw.id != child.id {
@@ -3991,14 +3968,8 @@ open class SyntaxRewriter {
39913968
}
39923969
}
39933970

3994-
if recyclableNodeInfos.capacity > recyclableNodeInfos.count,
3995-
isKnownUniquelyReferenced(&childNode.info) {
3996-
var info: Syntax.Info! = nil
3997-
// 'swap' to avoid ref-counting traffic.
3998-
swap(&childNode.info, &info)
3999-
info.info = nil
4000-
recyclableNodeInfos.append(info)
4001-
}
3971+
// Recycle 'childNode.info'
3972+
nodeFactory.dispose(&childNode)
40023973
}
40033974

40043975
if let newLayout {

0 commit comments

Comments
 (0)