Skip to content

Commit 81015bd

Browse files
committed
Make SyntaxIndexInTree public and serializable into a UInt64
If you have two copies of the same syntax tree within two processes, this allows us to communicate the identifier of a node in one process the the equivalent node in the other process.
1 parent 1672a3e commit 81015bd

File tree

7 files changed

+167
-55
lines changed

7 files changed

+167
-55
lines changed

Release Notes/600.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,22 @@
7575
- Description: With the change to parse `#if canImport(MyModule, _version: 1.2.3)` as a function call instead of a dedicated syntax node, `1.2.3` natively gets parsed as a member access `3` to the `1.2` float literal. This property allows the reinterpretation of such an expression as a version tuple.
7676
- Pull request: https://github.com/apple/swift-syntax/pull/2025
7777

78+
- `SyntaxProtocol.node(at:)`
79+
- Description: Given a `SyntaxIdentifier`, returns the `Syntax` node with that identifier
80+
- Pull request: https://github.com/apple/swift-syntax/pull/2594
81+
82+
- `SyntaxIdentifier.IndexInTree`
83+
- Description: Uniquely identifies a syntax node within a tree. This is similar to ``SyntaxIdentifier`` but does not store the root ID of the tree. It can thus be transferred across trees that are structurally equivalent, for example two copies of the same tree that live in different processes. The only public functions on this type are `toOpaque` and `init(fromOpaque:)`, which allow serialization of the `IndexInTree`.
84+
- Pull request: https://github.com/apple/swift-syntax/pull/2594
85+
86+
- `SyntaxIdentifier` conformance to `Comparable`:
87+
- Description: A `SyntaxIdentifier` compares less than another `SyntaxIdentifier` if the node at that identifier occurs first during a depth-first traversal of the tree.
88+
- Pull request: https://github.com/apple/swift-syntax/pull/2594
89+
90+
- `SyntaxIdentifier.indexInTree` and `SyntaxIdentifier.fromIndexInTree`
91+
- Description: `SyntaxIdentifier.indexInTree` allows the retrieval of a `SyntaxIdentifier` that identifies the syntax node independent of the syntax tree. `SyntaxIdentifier.fromIndexInTree` allows the creation for a `SyntaxIdentifier` from a tree-agnostic `SyntaxIdentifier.IndexInTree` and the tree's root node.
92+
- Pull request: https://github.com/apple/swift-syntax/pull/2594
93+
7894
## API Behavior Changes
7995

8096
## Deprecations

Sources/SwiftParserDiagnostics/ParseDiagnosticsGenerator.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@
1313
#if swift(>=6)
1414
public import SwiftDiagnostics
1515
@_spi(Diagnostics) import SwiftParser
16-
@_spi(RawSyntax) @_spi(ExperimentalLanguageFeatures) public import SwiftSyntax
16+
@_spi(ExperimentalLanguageFeatures) public import SwiftSyntax
1717
#else
1818
import SwiftDiagnostics
1919
@_spi(Diagnostics) import SwiftParser
20-
@_spi(RawSyntax) @_spi(ExperimentalLanguageFeatures) import SwiftSyntax
20+
@_spi(ExperimentalLanguageFeatures) import SwiftSyntax
2121
#endif
2222

2323
fileprivate func getTokens(between first: TokenSyntax, and second: TokenSyntax) -> [TokenSyntax] {
@@ -119,7 +119,7 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
119119
return false
120120
} else {
121121
// If multiple tokens are missing at the same location, emit diagnostics about nodes that occur earlier in the tree first.
122-
return $0.node.id.indexInTree < $1.node.id.indexInTree
122+
return $0.node.id < $1.node.id
123123
}
124124
}
125125
return diagProducer.diagnostics

Sources/SwiftSyntax/CommonAncestor.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@ public func findCommonAncestorOrSelf(_ lhs: Syntax, _ rhs: Syntax) -> Syntax? {
1818
if lhs == rhs {
1919
return lhs
2020
}
21-
if let lhsIndex = lhs?.indexInParent.data?.indexInTree, let rhsIndex = rhs?.indexInParent.data?.indexInTree {
22-
if lhsIndex < rhsIndex {
23-
rhs = rhs?.parent
21+
if let lhsUnwrapped = lhs, let rhsUnwrapped = rhs {
22+
if lhsUnwrapped.id < rhsUnwrapped.id {
23+
rhs = rhsUnwrapped.parent
2424
} else {
25-
lhs = lhs?.parent
25+
lhs = lhsUnwrapped.parent
2626
}
2727
}
2828
}

Sources/SwiftSyntax/SyntaxChildren.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ struct SyntaxChildrenIndexData: Hashable, Comparable, Sendable {
2222
/// See `AbsoluteSyntaxPosition.indexInParent`
2323
let indexInParent: UInt32
2424
/// Unique value for a node within its own tree.
25-
/// See `SyntaxIdentifier.indexIntree`
26-
let indexInTree: SyntaxIndexInTree
25+
/// See ``SyntaxIdentifier/indexInTree``
26+
let indexInTree: SyntaxIdentifier.SyntaxIndexInTree
2727

2828
static func < (
2929
lhs: SyntaxChildrenIndexData,
@@ -35,7 +35,7 @@ struct SyntaxChildrenIndexData: Hashable, Comparable, Sendable {
3535
fileprivate init(
3636
offset: UInt32,
3737
indexInParent: UInt32,
38-
indexInTree: SyntaxIndexInTree
38+
indexInTree: SyntaxIdentifier.SyntaxIndexInTree
3939
) {
4040
self.offset = offset
4141
self.indexInParent = indexInParent
@@ -72,7 +72,7 @@ public struct SyntaxChildrenIndex: Hashable, Comparable, ExpressibleByNilLiteral
7272
fileprivate init(
7373
offset: UInt32,
7474
indexInParent: UInt32,
75-
indexInTree: SyntaxIndexInTree
75+
indexInTree: SyntaxIdentifier.SyntaxIndexInTree
7676
) {
7777
self.data = SyntaxChildrenIndexData(
7878
offset: offset,
@@ -222,7 +222,7 @@ struct RawSyntaxChildren: BidirectionalCollection, Sendable {
222222
let offset = startIndex.offset + UInt32(parent.totalLength.utf8Length)
223223
let indexInParent = startIndex.indexInParent + UInt32(parentLayoutView.children.count)
224224
let indexInTree = startIndex.indexInTree.indexInTree + UInt32(parent.totalNodes) - 1
225-
let syntaxIndexInTree = SyntaxIndexInTree(indexInTree: indexInTree)
225+
let syntaxIndexInTree = SyntaxIdentifier.SyntaxIndexInTree(indexInTree: indexInTree)
226226
let materialized = SyntaxChildrenIndex(
227227
offset: offset,
228228
indexInParent: indexInParent,

Sources/SwiftSyntax/SyntaxIdentifier.swift

Lines changed: 95 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -10,42 +10,6 @@
1010
//
1111
//===----------------------------------------------------------------------===//
1212

13-
/// Represents a unique value for a node within its own tree.
14-
@_spi(RawSyntax)
15-
public struct SyntaxIndexInTree: Comparable, Hashable, Sendable {
16-
let indexInTree: UInt32
17-
18-
static let zero: SyntaxIndexInTree = SyntaxIndexInTree(indexInTree: 0)
19-
20-
/// Assuming that this index points to the start of ``Raw``, so that it points
21-
/// to the next sibling of ``Raw``.
22-
func advancedBy(_ raw: RawSyntax?) -> SyntaxIndexInTree {
23-
let newIndexInTree = self.indexInTree + UInt32(truncatingIfNeeded: raw?.totalNodes ?? 0)
24-
return .init(indexInTree: newIndexInTree)
25-
}
26-
27-
/// Assuming that this index points to the next sibling of ``Raw``, reverse it
28-
/// so that it points to the start of ``Raw``.
29-
func reversedBy(_ raw: RawSyntax?) -> SyntaxIndexInTree {
30-
let newIndexInTree = self.indexInTree - UInt32(truncatingIfNeeded: raw?.totalNodes ?? 0)
31-
return .init(indexInTree: newIndexInTree)
32-
}
33-
34-
func advancedToFirstChild() -> SyntaxIndexInTree {
35-
let newIndexInTree = self.indexInTree + 1
36-
return .init(indexInTree: newIndexInTree)
37-
}
38-
39-
init(indexInTree: UInt32) {
40-
self.indexInTree = indexInTree
41-
}
42-
43-
/// Returns `true` if `lhs` occurs before `rhs` in the tree.
44-
public static func < (lhs: SyntaxIndexInTree, rhs: SyntaxIndexInTree) -> Bool {
45-
return lhs.indexInTree < rhs.indexInTree
46-
}
47-
}
48-
4913
/// Provides a stable and unique identity for ``Syntax`` nodes.
5014
///
5115
/// Note that two nodes might have the same contents even if their IDs are
@@ -57,7 +21,53 @@ public struct SyntaxIndexInTree: Comparable, Hashable, Sendable {
5721
/// different syntax tree. Modifying any node in the syntax tree a node is
5822
/// contained in generates a copy of that tree and thus changes the IDs of all
5923
/// nodes in the tree, not just the modified node's children.
60-
public struct SyntaxIdentifier: Hashable, Sendable {
24+
public struct SyntaxIdentifier: Comparable, Hashable, Sendable {
25+
/// Represents a unique value for a node within its own tree.
26+
///
27+
/// This is similar to ``SyntaxIdentifier`` but does not store the root ID of the tree.
28+
/// It can thus be transferred across trees that are structurally equivalent, for example two copies of the same tree
29+
/// that live in different processes.
30+
public struct SyntaxIndexInTree: Hashable, Sendable {
31+
/// When traversing the syntax tree using a depth-first traversal, the index at which the node will be visited.
32+
let indexInTree: UInt32
33+
34+
/// Assuming that this index points to the start of `raw`, advance it so that it points to the next sibling of
35+
/// `raw`.
36+
func advancedBy(_ raw: RawSyntax?) -> SyntaxIndexInTree {
37+
let newIndexInTree = self.indexInTree + UInt32(truncatingIfNeeded: raw?.totalNodes ?? 0)
38+
return .init(indexInTree: newIndexInTree)
39+
}
40+
41+
/// Assuming that this index points to the next sibling of `raw`, reverse it so that it points to the start of
42+
/// `raw`.
43+
func reversedBy(_ raw: RawSyntax?) -> SyntaxIndexInTree {
44+
let newIndexInTree = self.indexInTree - UInt32(truncatingIfNeeded: raw?.totalNodes ?? 0)
45+
return .init(indexInTree: newIndexInTree)
46+
}
47+
48+
func advancedToFirstChild() -> SyntaxIndexInTree {
49+
let newIndexInTree = self.indexInTree + 1
50+
return .init(indexInTree: newIndexInTree)
51+
}
52+
53+
init(indexInTree: UInt32) {
54+
self.indexInTree = indexInTree
55+
}
56+
57+
/// Converts the ``SyntaxIdentifier/SyntaxIndexInTree`` to an opaque value that can be serialized.
58+
/// The opaque value can be restored to a ``SyntaxIdentifier/SyntaxIndexInTree`` using ``init(fromOpaque:)``.
59+
///
60+
/// - Note: The contents of the opaque value are not specified and clients should not rely on them.
61+
public func toOpaque() -> UInt64 {
62+
return UInt64(indexInTree)
63+
}
64+
65+
/// Creates a ``SyntaxIdentifier/SyntaxIndexInTree`` from an opaque value obtained using ``toOpaque()``.
66+
public init(fromOpaque opaque: UInt64) {
67+
self.indexInTree = UInt32(opaque)
68+
}
69+
}
70+
6171
/// Unique value for the root node.
6272
///
6373
/// Multiple trees may have the same 'rootId' if their root RawSyntax is the
@@ -67,23 +77,65 @@ public struct SyntaxIdentifier: Hashable, Sendable {
6777
let rootId: UInt
6878

6979
/// Unique value for a node within its own tree.
70-
@_spi(RawSyntax)
7180
public let indexInTree: SyntaxIndexInTree
7281

82+
/// Returns the `UInt` that is used as the root ID for the given raw syntax node.
83+
private static func rootId(of raw: RawSyntax) -> UInt {
84+
return UInt(bitPattern: raw.pointer.unsafeRawPointer)
85+
}
86+
7387
func advancedBySibling(_ raw: RawSyntax?) -> SyntaxIdentifier {
7488
let newIndexInTree = indexInTree.advancedBy(raw)
75-
return .init(rootId: self.rootId, indexInTree: newIndexInTree)
89+
return SyntaxIdentifier(rootId: self.rootId, indexInTree: newIndexInTree)
7690
}
7791

7892
func advancedToFirstChild() -> SyntaxIdentifier {
7993
let newIndexInTree = self.indexInTree.advancedToFirstChild()
80-
return .init(rootId: self.rootId, indexInTree: newIndexInTree)
94+
return SyntaxIdentifier(rootId: self.rootId, indexInTree: newIndexInTree)
8195
}
8296

8397
static func forRoot(_ raw: RawSyntax) -> SyntaxIdentifier {
84-
return .init(
85-
rootId: UInt(bitPattern: raw.pointer.unsafeRawPointer),
86-
indexInTree: .zero
98+
return SyntaxIdentifier(
99+
rootId: Self.rootId(of: raw),
100+
indexInTree: SyntaxIndexInTree(indexInTree: 0)
87101
)
88102
}
103+
104+
/// Forms a ``SyntaxIdentifier`` from an ``SyntaxIdentifier/SyntaxIndexInTree`` inside a ``Syntax`` node that
105+
/// constitutes the tree's root.
106+
///
107+
/// Returns `nil` if `root` is not the root of a syntax tree or if `indexInTree` points to a node that is not within
108+
/// the tree spanned up by `root`.
109+
///
110+
/// - Warning: ``SyntaxIdentifier/SyntaxIndexInTree`` is not stable with regard to insertion or deletions of nodes
111+
/// into a syntax tree. There are only two scenarios where it is valid to share ``SyntaxIndexInTree`` between syntax
112+
/// trees with different nodes:
113+
/// (1) If two trees are guaranteed to be exactly the same eg. because they were parsed using the same version of
114+
/// `SwiftParser` from the same source code.
115+
/// (2) If a tree was mutated by only replacing tokens with other tokens. No nodes must have been inserted or
116+
/// removed during the process, including tokens that are marked as ``SourcePresence/missing``.
117+
public static func fromIndexInTree(
118+
_ indexInTree: SyntaxIndexInTree,
119+
relativeToRoot root: some SyntaxProtocol
120+
) -> SyntaxIdentifier? {
121+
guard !root.hasParent else {
122+
return nil
123+
}
124+
guard indexInTree.indexInTree < SyntaxIndexInTree(indexInTree: 0).advancedBy(root.raw).indexInTree else {
125+
return nil
126+
}
127+
128+
return SyntaxIdentifier(rootId: Self.rootId(of: root.raw), indexInTree: indexInTree)
129+
}
130+
131+
/// A ``SyntaxIdentifier`` compares less than another ``SyntaxIdentifier`` if the node at that identifier occurs first
132+
/// during a depth-first traversal of the tree. This implies that nodes with an earlier ``AbsolutePosition`` also
133+
/// have a lower ``SyntaxIdentifier``.
134+
public static func < (lhs: SyntaxIdentifier, rhs: SyntaxIdentifier) -> Bool {
135+
guard lhs.rootId == rhs.rootId else {
136+
// Nodes in different trees are not comparable.
137+
return false
138+
}
139+
return lhs.indexInTree.indexInTree < rhs.indexInTree.indexInTree
140+
}
89141
}

Sources/SwiftSyntax/SyntaxProtocol.swift

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,28 @@ public extension SyntaxProtocol {
352352

353353
fatalError("Children of syntax node do not cover all positions in it")
354354
}
355+
356+
/// If the node with the given `syntaxIdentifier` is a (recursive) child of this node, return the node with that
357+
/// identifier.
358+
///
359+
/// If the identifier references a node from a different tree (ie. one that has a different root ID in the
360+
/// ``SyntaxIdentifier``) or if no node with the given identifier is a child of this syntax node, returns `nil`.
361+
func node(at syntaxIdentifier: SyntaxIdentifier) -> Syntax? {
362+
guard self.id <= syntaxIdentifier && syntaxIdentifier < self.id.advancedBySibling(self.raw) else {
363+
// The syntax identifier is not part of this tree.
364+
return nil
365+
}
366+
if self.id == syntaxIdentifier {
367+
return Syntax(self)
368+
}
369+
for child in children(viewMode: .all) {
370+
if let node = child.node(at: syntaxIdentifier) {
371+
return node
372+
}
373+
}
374+
375+
preconditionFailure("syntaxIdentifier is covered by this node but not any of its children?")
376+
}
355377
}
356378

357379
// MARK: Recursive flags

Tests/SwiftSyntaxTest/SyntaxTests.swift

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,4 +146,26 @@ class SyntaxTests: XCTestCase {
146146
let node = ClosureCaptureSyntax(name: "test", expression: ExprSyntax("123"))
147147
XCTAssertEqual(node.formatted().description, "test = 123")
148148
}
149+
150+
func testShareSyntaxIndexInTreeBetweenTrees() throws {
151+
let source = "func foo() {}"
152+
153+
let tree1 = DeclSyntax(stringLiteral: source)
154+
let tree2 = DeclSyntax(stringLiteral: source)
155+
156+
let funcKeywordInTree1 = try XCTUnwrap(tree1.firstToken(viewMode: .sourceAccurate))
157+
XCTAssertEqual(funcKeywordInTree1.tokenKind, .keyword(.func))
158+
159+
let opaqueIndexInTree1 = funcKeywordInTree1.id.indexInTree.toOpaque()
160+
161+
let funcKeywordIdentifierInTree2 = try XCTUnwrap(
162+
SyntaxIdentifier.fromIndexInTree(
163+
SyntaxIdentifier.SyntaxIndexInTree(fromOpaque: opaqueIndexInTree1),
164+
relativeToRoot: tree2
165+
)
166+
)
167+
let funcKeywordInTree2 = tree2.node(at: funcKeywordIdentifierInTree2)
168+
XCTAssertEqual(funcKeywordInTree2?.as(TokenSyntax.self)?.tokenKind, .keyword(.func))
169+
XCTAssertNotEqual(funcKeywordInTree1.id, funcKeywordInTree2?.id)
170+
}
149171
}

0 commit comments

Comments
 (0)