Skip to content

Commit 7031ea3

Browse files
committed
[NFC-ish] Factor out CompatibilityLayer
A new CompatibilityLayer type computes information about deprecated vars and inits, presenting it in a new way: by creating additional `Child` objects with the deprecated information, plus sufficient info to map it back to the current name. This causes minor non-functional changes to the bodies of compatibility builder inits, but otherwise has no real effect in this commit. However, this new setup with separate `Child` nodes for deprecated children and a global table of information about them prepares us for further changes.
1 parent 8e12022 commit 7031ea3

File tree

8 files changed

+401
-181
lines changed

8 files changed

+401
-181
lines changed

CodeGeneration/Sources/SyntaxSupport/Child.swift

+76-16
Original file line numberDiff line numberDiff line change
@@ -150,29 +150,42 @@ public class Child: NodeChoiceConvertible {
150150
/// For any other kind of child nodes, accessing this property crashes.
151151
public var syntaxChoicesType: TypeSyntax {
152152
precondition(kind.isNodeChoices, "Cannot get `syntaxChoicesType` for node that doesn’t have nodeChoices")
153-
return "\(raw: name.withFirstCharacterUppercased)"
153+
return "\(raw: newestName.withFirstCharacterUppercased)"
154154
}
155155

156156
/// If this child only has tokens, the type that the generated `TokenSpecSet` should get.
157157
///
158158
/// For any other kind of child nodes, accessing this property crashes.
159159
public var tokenSpecSetType: TypeSyntax {
160160
precondition(kind.isToken, "Cannot get `tokenSpecSetType` for node that isn’t a token")
161-
return "\(raw: name.withFirstCharacterUppercased)Options"
162-
}
163-
164-
/// The deprecated name of this child that's suitable to be used for variable or enum case names.
165-
public var deprecatedVarName: TokenSyntax? {
166-
guard let deprecatedName = deprecatedName else {
167-
return nil
168-
}
169-
return .identifier(lowercaseFirstWord(name: deprecatedName))
161+
return "\(raw: newestName.withFirstCharacterUppercased)Options"
170162
}
171163

172164
/// Determines if this child has a deprecated name
173165
public var hasDeprecatedName: Bool {
174166
return deprecatedName != nil
175167
}
168+
169+
/// If this child is actually part of another child's history, links back
170+
/// to the newest (that is, most current/non-deprecated) version of the
171+
/// child. Nil if this is the newest version of the child.
172+
public let newestChild: Child?
173+
174+
/// True if this child was created by a `Child.Refactoring`. Such children
175+
/// are part of the compatibility layer and are therefore deprecated.
176+
public var isHistorical: Bool {
177+
newestChild != nil
178+
}
179+
180+
/// Replaces the nodes in `newerChildPath` with their own `newerChildPath`s,
181+
/// if any, to form a child path enitrely of non-historical nodes.
182+
static private func makeNewestChild(from newerChild: Child?) -> Child? {
183+
return newerChild?.newestChild ?? newerChild
184+
}
185+
186+
private var newestName: String {
187+
return newestChild?.name ?? name
188+
}
176189

177190
/// If the child ends with "token" in the kind, it's considered a token node.
178191
/// Grab the existing reference to that token from the global list.
@@ -244,19 +257,15 @@ public class Child: NodeChoiceConvertible {
244257
return AttributeListSyntax("@_spi(ExperimentalLanguageFeatures)").with(\.trailingTrivia, .newline)
245258
}
246259

247-
/// If a classification is passed, it specifies the color identifiers in
248-
/// that subtree should inherit for syntax coloring. Must be a member of
249-
/// ``SyntaxClassification``.
250-
/// If `forceClassification` is also set to true, all child nodes (not only
251-
/// identifiers) inherit the syntax classification.
252260
init(
253261
name: String,
254262
deprecatedName: String? = nil,
255263
kind: ChildKind,
256264
experimentalFeature: ExperimentalFeature? = nil,
257265
nameForDiagnostics: String? = nil,
258266
documentation: String? = nil,
259-
isOptional: Bool = false
267+
isOptional: Bool = false,
268+
newerChild: Child? = nil
260269
) {
261270
precondition(name.first?.isLowercase ?? true, "The first letter of a child’s name should be lowercase")
262271
precondition(
@@ -265,11 +274,62 @@ public class Child: NodeChoiceConvertible {
265274
)
266275
self.name = name
267276
self.deprecatedName = deprecatedName
277+
self.newestChild = Self.makeNewestChild(from: newerChild)
268278
self.kind = kind
269279
self.experimentalFeature = experimentalFeature
270280
self.nameForDiagnostics = nameForDiagnostics
271281
self.documentationSummary = SwiftSyntax.Trivia.docCommentTrivia(from: documentation)
272282
self.documentationAbstract = String(documentation?.split(whereSeparator: \.isNewline).first ?? "")
273283
self.isOptional = isOptional
274284
}
285+
286+
/// Create a node that is a copy of the last node in `newerChildPath`, but
287+
/// with modifications.
288+
init(renamingTo replacementName: String? = nil, newerChild other: Child) {
289+
self.name = replacementName ?? other.name
290+
self.deprecatedName = nil
291+
self.newestChild = Self.makeNewestChild(from: other)
292+
self.kind = other.kind
293+
self.experimentalFeature = other.experimentalFeature
294+
self.nameForDiagnostics = other.nameForDiagnostics
295+
self.documentationSummary = other.documentationSummary
296+
self.documentationAbstract = other.documentationAbstract
297+
self.isOptional = other.isOptional
298+
}
299+
300+
/// Create a child for the unexpected nodes between two children (either or
301+
/// both of which may be `nil`).
302+
convenience init(forUnexpectedBetween earlier: Child?, and later: Child?, newerChild: Child? = nil) {
303+
let name = switch (earlier, later) {
304+
case (nil, let later?):
305+
"unexpectedBefore\(later.name.withFirstCharacterUppercased)"
306+
case (let earlier?, nil):
307+
"unexpectedAfter\(earlier.name.withFirstCharacterUppercased)"
308+
case (let earlier?, let later?):
309+
"unexpectedBetween\(earlier.name.withFirstCharacterUppercased)And\(later.name.withFirstCharacterUppercased)"
310+
case (nil, nil):
311+
fatalError("unexpected node has no siblings?")
312+
}
313+
314+
self.init(
315+
name: name,
316+
deprecatedName: nil, // deprecation of unexpected nodes is handled in CompatibilityLayer
317+
kind: .collection(kind: .unexpectedNodes, collectionElementName: name.withFirstCharacterUppercased),
318+
experimentalFeature: earlier?.experimentalFeature ?? later?.experimentalFeature,
319+
nameForDiagnostics: nil,
320+
documentation: nil,
321+
isOptional: true,
322+
newerChild: newerChild
323+
)
324+
}
325+
}
326+
327+
extension Child: Hashable {
328+
public static func == (lhs: Child, rhs: Child) -> Bool {
329+
lhs === rhs
330+
}
331+
332+
public func hash(into hasher: inout Hasher) {
333+
hasher.combine(ObjectIdentifier(self))
334+
}
275335
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 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+
/// Computes and caches information about properties and initializers that ought to be generated for the compatibility layer.
14+
public struct CompatibilityLayer {
15+
/// Deprecated members that the compatibility layer needs for each node.
16+
private var deprecatedMembersByNode: [SyntaxNodeKind: DeprecatedMemberInfo] = [:]
17+
18+
/// Cache for `replacementChild(for:)`. Ensures that we don't create two different replacement children even
19+
/// if we refactor the same child twice, so we can reliably equate and hash `Child` objects by object identity.
20+
private var cachedReplacementChildren: [Child: Child?] = [:]
21+
22+
/// Returns the deprecated members that the compatibility layer needs for `node`.
23+
public func deprecatedMembers(for node: LayoutNode) -> DeprecatedMemberInfo {
24+
return deprecatedMembersByNode[node.kind] ?? DeprecatedMemberInfo()
25+
}
26+
27+
internal init(nodes: [Node]) {
28+
// This instance will be stored in a global that's used from multiple threads simultaneously, so it won't be safe
29+
// to mutate once the initializer returns. We therefore do all the work to populate its tables up front, rather
30+
// than computing it lazily on demand.
31+
for node in nodes {
32+
computeMembers(for: node)
33+
}
34+
}
35+
36+
private mutating func replacementChild(for newerChild: Child) -> Child? {
37+
func make() -> Child? {
38+
guard let deprecatedName = newerChild.deprecatedName else {
39+
return nil
40+
}
41+
42+
return Child(renamingTo: deprecatedName, newerChild: newerChild)
43+
}
44+
45+
// Make sure we return the same instance even if we're called twice.
46+
if cachedReplacementChildren[newerChild] == nil {
47+
cachedReplacementChildren[newerChild] = make()
48+
}
49+
return cachedReplacementChildren[newerChild]!
50+
}
51+
52+
private mutating func computeMembers(for node: Node) {
53+
guard deprecatedMembersByNode[node.syntaxNodeKind] == nil, let layoutNode = node.layoutNode else {
54+
return
55+
}
56+
57+
// The results that will ultimately be saved into the DeprecatedMemberInfo.
58+
var vars: [Child] = []
59+
var initSignatures: [InitSignature] = []
60+
61+
// Temporary working state.
62+
var children = layoutNode.children
63+
var knownVars = Set(children)
64+
65+
func firstIndexOfChild(named targetName: String) -> Int {
66+
guard let i = children.firstIndex(where: { $0.name == targetName }) else {
67+
fatalError("couldn't find '\(targetName)' in current children of \(node.syntaxNodeKind.rawValue): \(String(reflecting: children.map(\.name)))")
68+
}
69+
return i
70+
}
71+
72+
var unexpectedChildrenWithNewNames: Set<Child> = []
73+
74+
// First pass: Apply the changes explicitly specified in the change set.
75+
for i in children.indices {
76+
let currentName = children[i].name
77+
guard let replacementChild = replacementChild(for: children[i]) else {
78+
continue
79+
}
80+
children[i] = replacementChild
81+
82+
// Mark adjacent unexpected node children whose names have changed too.
83+
if currentName != replacementChild.name {
84+
unexpectedChildrenWithNewNames.insert(children[i - 1])
85+
unexpectedChildrenWithNewNames.insert(children[i + 1])
86+
}
87+
}
88+
89+
// Second pass: Update unexpected node children adjacent to those changes whose names have probably changed.
90+
for unexpectedChild in unexpectedChildrenWithNewNames {
91+
precondition(unexpectedChild.isUnexpectedNodes)
92+
let i = firstIndexOfChild(named: unexpectedChild.name)
93+
94+
let earlier = children[checked: i - 1]
95+
let later = children[checked: i + 1]
96+
precondition(!(earlier?.isUnexpectedNodes ?? false) && !(later?.isUnexpectedNodes ?? false))
97+
98+
let newChild = Child(forUnexpectedBetween: earlier, and: later, newerChild: unexpectedChild)
99+
precondition(newChild.name != unexpectedChild.name)
100+
precondition(!children.contains { $0.name == newChild.name })
101+
102+
children[i] = newChild
103+
}
104+
105+
// Third pass: Append newly-created children to vars. We do this now so that changes from the first two passes are properly interleaved, preserving source order.
106+
vars += children.filter { knownVars.insert($0).inserted }
107+
108+
initSignatures.append(InitSignature(children: children))
109+
110+
deprecatedMembersByNode[node.syntaxNodeKind] = DeprecatedMemberInfo(vars: vars, inits: initSignatures)
111+
}
112+
}
113+
114+
/// Describes the deprecated members of a given type that the compatibility layer ought to provide.
115+
public struct DeprecatedMemberInfo {
116+
/// Properties that are needed in the compatibility layer, in the order they ought to appear in the generated file.
117+
public var vars: [Child] = []
118+
119+
/// Initializer signatures that are needed in the compatibility layer, in the order they ought to appear in the generated file.
120+
public var inits: [InitSignature] = []
121+
}
122+
123+
extension Array {
124+
/// Returns `nil` if `i` is out of bounds, or the indicated element otherwise.
125+
fileprivate subscript(checked i: Index) -> Element? {
126+
get {
127+
return indices.contains(i) ? self[i] : nil
128+
}
129+
}
130+
}

CodeGeneration/Sources/SyntaxSupport/Node.swift

+4-52
Original file line numberDiff line numberDiff line change
@@ -135,58 +135,10 @@ public class Node: NodeChoiceConvertible {
135135
self.documentation = SwiftSyntax.Trivia.docCommentTrivia(from: documentation)
136136
self.parserFunction = parserFunction
137137

138-
let childrenWithUnexpected: [Child]
139-
if children.isEmpty {
140-
childrenWithUnexpected = [
141-
Child(
142-
name: "unexpected",
143-
kind: .collection(kind: .unexpectedNodes, collectionElementName: "Unexpected"),
144-
isOptional: true
145-
)
146-
]
147-
} else {
148-
// Add implicitly generated UnexpectedNodes children between
149-
// any two defined children
150-
childrenWithUnexpected =
151-
children.enumerated().flatMap { (i, child) -> [Child] in
152-
let childName = child.name.withFirstCharacterUppercased
153-
154-
let unexpectedName: String
155-
let unexpectedDeprecatedName: String?
156-
157-
if i == 0 {
158-
unexpectedName = "unexpectedBefore\(childName)"
159-
unexpectedDeprecatedName = child.deprecatedName.map { "unexpectedBefore\($0.withFirstCharacterUppercased)" }
160-
} else {
161-
unexpectedName = "unexpectedBetween\(children[i - 1].name.withFirstCharacterUppercased)And\(childName)"
162-
if let deprecatedName = children[i - 1].deprecatedName?.withFirstCharacterUppercased {
163-
unexpectedDeprecatedName =
164-
"unexpectedBetween\(deprecatedName)And\(child.deprecatedName?.withFirstCharacterUppercased ?? childName)"
165-
} else if let deprecatedName = child.deprecatedName?.withFirstCharacterUppercased {
166-
unexpectedDeprecatedName =
167-
"unexpectedBetween\(children[i - 1].name.withFirstCharacterUppercased)And\(deprecatedName)"
168-
} else {
169-
unexpectedDeprecatedName = nil
170-
}
171-
}
172-
let unexpectedBefore = Child(
173-
name: unexpectedName,
174-
deprecatedName: unexpectedDeprecatedName,
175-
kind: .collection(kind: .unexpectedNodes, collectionElementName: unexpectedName),
176-
isOptional: true
177-
)
178-
return [unexpectedBefore, child]
179-
} + [
180-
Child(
181-
name: "unexpectedAfter\(children.last!.name.withFirstCharacterUppercased)",
182-
deprecatedName: children.last!.deprecatedName.map { "unexpectedAfter\($0.withFirstCharacterUppercased)" },
183-
kind: .collection(
184-
kind: .unexpectedNodes,
185-
collectionElementName: "UnexpectedAfter\(children.last!.name.withFirstCharacterUppercased)"
186-
),
187-
isOptional: true
188-
)
189-
]
138+
let liftedChildren = children.lazy.map(Optional.some)
139+
let pairedChildren = zip([nil] + liftedChildren, liftedChildren + [nil])
140+
let childrenWithUnexpected = pairedChildren.flatMap { earlier, later in
141+
[earlier, Child(forUnexpectedBetween: earlier, and: later)].compactMap { $0 }
190142
}
191143
self.data = .layout(children: childrenWithUnexpected, traits: traits)
192144
}

CodeGeneration/Sources/SyntaxSupport/SyntaxNodes.swift

+2
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,5 @@ public let SYNTAX_NODE_MAP: [SyntaxNodeKind: Node] = Dictionary(
3535
)
3636

3737
public let NON_BASE_SYNTAX_NODES = SYNTAX_NODES.filter { !$0.kind.isBase }
38+
39+
public let SYNTAX_COMPATIBILITY_LAYER = CompatibilityLayer(nodes: SYNTAX_NODES)

0 commit comments

Comments
 (0)