Skip to content

Commit ced0712

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 4567192 commit ced0712

File tree

8 files changed

+413
-185
lines changed

8 files changed

+413
-185
lines changed

CodeGeneration/Sources/SyntaxSupport/Child.swift

Lines changed: 77 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -150,30 +150,43 @@ 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
}
176168

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+
}
189+
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.
179192
public var tokenKind: Token? {
@@ -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,63 @@ 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 =
304+
switch (earlier, later) {
305+
case (nil, let later?):
306+
"unexpectedBefore\(later.name.withFirstCharacterUppercased)"
307+
case (let earlier?, nil):
308+
"unexpectedAfter\(earlier.name.withFirstCharacterUppercased)"
309+
case (let earlier?, let later?):
310+
"unexpectedBetween\(earlier.name.withFirstCharacterUppercased)And\(later.name.withFirstCharacterUppercased)"
311+
case (nil, nil):
312+
fatalError("unexpected node has no siblings?")
313+
}
314+
315+
self.init(
316+
name: name,
317+
deprecatedName: nil, // deprecation of unexpected nodes is handled in CompatibilityLayer
318+
kind: .collection(kind: .unexpectedNodes, collectionElementName: name.withFirstCharacterUppercased),
319+
experimentalFeature: earlier?.experimentalFeature ?? later?.experimentalFeature,
320+
nameForDiagnostics: nil,
321+
documentation: nil,
322+
isOptional: true,
323+
newerChild: newerChild
324+
)
325+
}
326+
}
327+
328+
extension Child: Hashable {
329+
public static func == (lhs: Child, rhs: Child) -> Bool {
330+
lhs === rhs
331+
}
332+
333+
public func hash(into hasher: inout Hasher) {
334+
hasher.combine(ObjectIdentifier(self))
335+
}
275336
}
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
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(
68+
"couldn't find '\(targetName)' in current children of \(node.syntaxNodeKind.rawValue): \(String(reflecting: children.map(\.name)))"
69+
)
70+
}
71+
return i
72+
}
73+
74+
var unexpectedChildrenWithNewNames: Set<Child> = []
75+
76+
// First pass: Apply the changes explicitly specified in the change set.
77+
for i in children.indices {
78+
let currentName = children[i].name
79+
guard let replacementChild = replacementChild(for: children[i]) else {
80+
continue
81+
}
82+
children[i] = replacementChild
83+
84+
// Mark adjacent unexpected node children whose names have changed too.
85+
if currentName != replacementChild.name {
86+
unexpectedChildrenWithNewNames.insert(children[i - 1])
87+
unexpectedChildrenWithNewNames.insert(children[i + 1])
88+
}
89+
}
90+
91+
// Second pass: Update unexpected node children adjacent to those changes whose names have probably changed.
92+
for unexpectedChild in unexpectedChildrenWithNewNames {
93+
precondition(unexpectedChild.isUnexpectedNodes)
94+
let i = firstIndexOfChild(named: unexpectedChild.name)
95+
96+
let earlier = children[checked: i - 1]
97+
let later = children[checked: i + 1]
98+
precondition(!(earlier?.isUnexpectedNodes ?? false) && !(later?.isUnexpectedNodes ?? false))
99+
100+
let newChild = Child(forUnexpectedBetween: earlier, and: later, newerChild: unexpectedChild)
101+
precondition(newChild.name != unexpectedChild.name)
102+
precondition(!children.contains { $0.name == newChild.name })
103+
104+
children[i] = newChild
105+
}
106+
107+
// 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.
108+
vars += children.filter { knownVars.insert($0).inserted }
109+
110+
initSignatures.append(InitSignature(children: children))
111+
112+
deprecatedMembersByNode[node.syntaxNodeKind] = DeprecatedMemberInfo(vars: vars, inits: initSignatures)
113+
}
114+
}
115+
116+
/// Describes the deprecated members of a given type that the compatibility layer ought to provide.
117+
public struct DeprecatedMemberInfo {
118+
/// Properties that are needed in the compatibility layer, in the order they ought to appear in the generated file.
119+
public var vars: [Child] = []
120+
121+
/// Initializer signatures that are needed in the compatibility layer, in the order they ought to appear in the generated file.
122+
public var inits: [InitSignature] = []
123+
}
124+
125+
extension Array {
126+
/// Returns `nil` if `i` is out of bounds, or the indicated element otherwise.
127+
fileprivate subscript(checked i: Index) -> Element? {
128+
get {
129+
return indices.contains(i) ? self[i] : nil
130+
}
131+
}
132+
}

CodeGeneration/Sources/SyntaxSupport/Node.swift

Lines changed: 11 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -135,59 +135,8 @@ 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-
]
190-
}
138+
let childrenWithUnexpected = kind.isBase ? children : interleaveUnexpectedChildren(children)
139+
191140
self.data = .layout(children: childrenWithUnexpected, traits: traits)
192141
}
193142

@@ -425,3 +374,12 @@ fileprivate extension Child {
425374
}
426375
}
427376
}
377+
378+
fileprivate func interleaveUnexpectedChildren(_ children: [Child]) -> [Child] {
379+
let liftedChildren = children.lazy.map(Optional.some)
380+
let pairedChildren = zip([nil] + liftedChildren, liftedChildren + [nil])
381+
382+
return pairedChildren.flatMap { earlier, later in
383+
[earlier, Child(forUnexpectedBetween: earlier, and: later)].compactMap { $0 }
384+
}
385+
}

CodeGeneration/Sources/SyntaxSupport/SyntaxNodes.swift

Lines changed: 2 additions & 0 deletions
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)