Skip to content

Commit 762ce11

Browse files
committed
[NFC] Generate code for experimental nodes better
Initializers for nodes with experimental node children need to be marked `@_spi`. This PR: • Adds that attribute. • Generates an alternative which *doesn’t* use SPI as part of the compatibility layer. • As a side effect, adds a `Child.Refactoring.introduced` case that can be used to generate compatibility `unexpected` properties. No functional change in this commit, but it will affect the code generation in the next one.
1 parent d848067 commit 762ce11

File tree

6 files changed

+293
-107
lines changed

6 files changed

+293
-107
lines changed

CodeGeneration/Sources/SyntaxSupport/Child.swift

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,19 @@ public class Child: NodeChoiceConvertible {
140140
}
141141
}
142142

143+
/// Should this child be hidden?
144+
///
145+
/// A hidden child is one that is not accessible in any way at a specific point in the history, but still needs to be
146+
/// (default) initialized. As always, its `newestChildPath` indicates the current way to access it.
147+
///
148+
/// Hidden children are used for `Refactoring.introduced` and for the implicit changeset that creates
149+
/// non-experimental APIs that ignore experimental children.
150+
public let isHidden: Bool
151+
152+
/// True if this child was created by a `childHistory` change set. Such children
153+
/// are part of the compatibility layer and are therefore deprecated.
154+
public var isHistorical: Bool
155+
143156
/// A name of this child as an identifier.
144157
public var identifier: TokenSyntax {
145158
return .identifier(lowercaseFirstWord(name: name))
@@ -161,8 +174,8 @@ public class Child: NodeChoiceConvertible {
161174
return "\(raw: newestName.withFirstCharacterUppercased)Options"
162175
}
163176

164-
/// If this child is deprecated, describes the sequence of accesses necessary
165-
/// to reach the equivalent value using non-deprecated children; if the child
177+
/// If this child is part of a compatibility layer, describes the sequence of accesses necessary
178+
/// to reach the equivalent value using non-compatibility-layer children; if the child
166179
/// is not deprecated, this array is empty.
167180
///
168181
/// Think of the elements of this array like components in a key path:
@@ -199,12 +212,6 @@ public class Child: NodeChoiceConvertible {
199212
/// of the child. That information is not directly available anywhere.
200213
public let newestChildPath: [Child]
201214

202-
/// True if this child was created by a `Child.Refactoring`. Such children
203-
/// are part of the compatibility layer and are therefore deprecated.
204-
public var isHistorical: Bool {
205-
!newestChildPath.isEmpty
206-
}
207-
208215
/// Replaces the nodes in `newerChildPath` with their own `newerChildPath`s,
209216
/// if any, to form a child path enitrely of non-historical nodes.
210217
static private func makeNewestChildPath(from newerChildPath: [Child]) -> [Child] {
@@ -214,7 +221,7 @@ public class Child: NodeChoiceConvertible {
214221
var workStack = Array(newerChildPath.reversed())
215222

216223
while let elem = workStack.popLast() {
217-
if elem.isHistorical {
224+
if !elem.newestChildPath.isEmpty {
218225
// There's an even newer version. Start working on that.
219226
workStack.append(contentsOf: elem.newestChildPath.reversed())
220227
} else {
@@ -308,7 +315,8 @@ public class Child: NodeChoiceConvertible {
308315
documentation: String? = nil,
309316
isOptional: Bool = false,
310317
providesDefaultInitialization: Bool = true,
311-
newerChildPath: [Child] = []
318+
newerChildPath: [Child] = [],
319+
isHistorical: Bool = false
312320
) {
313321
precondition(name.first?.isLowercase ?? true, "The first letter of a child’s name should be lowercase")
314322
self.name = name
@@ -320,11 +328,18 @@ public class Child: NodeChoiceConvertible {
320328
self.documentationAbstract = String(documentation?.split(whereSeparator: \.isNewline).first ?? "")
321329
self.isOptional = isOptional
322330
self.providesDefaultInitialization = providesDefaultInitialization
331+
self.isHidden = false
332+
self.isHistorical = isHistorical
323333
}
324334

325335
/// Create a node that is a copy of the last node in `newerChildPath`, but
326336
/// with modifications.
327-
init(renamingTo replacementName: String? = nil, newerChildPath: [Child]) {
337+
init(
338+
renamingTo replacementName: String? = nil,
339+
makingHistorical: Bool = false,
340+
makingHidden: Bool = false,
341+
newerChildPath: [Child]
342+
) {
328343
let other = newerChildPath.last!
329344

330345
self.name = replacementName ?? other.name
@@ -336,6 +351,8 @@ public class Child: NodeChoiceConvertible {
336351
self.documentationAbstract = other.documentationAbstract
337352
self.isOptional = other.isOptional
338353
self.providesDefaultInitialization = other.providesDefaultInitialization
354+
self.isHidden = makingHidden || other.isHidden
355+
self.isHistorical = makingHistorical || other.isHistorical
339356
}
340357

341358
/// Create a child for the unexpected nodes between two children (either or
@@ -361,7 +378,8 @@ public class Child: NodeChoiceConvertible {
361378
documentation: nil,
362379
isOptional: true,
363380
providesDefaultInitialization: true,
364-
newerChildPath: newerChildPath
381+
newerChildPath: newerChildPath,
382+
isHistorical: (earlier?.isHistorical ?? false) || (later?.isHistorical ?? false)
365383
)
366384
}
367385
}
@@ -417,5 +435,8 @@ extension Child {
417435
/// point in the past, so deprecated aliases that flatten the other node's
418436
/// children into this node should be provided.
419437
case extracted
438+
439+
/// A new child was added (and it's important to preserve the names around it).
440+
case introduced
420441
}
421442
}

CodeGeneration/Sources/SyntaxSupport/CompatibilityLayer.swift

Lines changed: 147 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@ public struct CompatibilityLayer {
1818
/// Deprecated members that the compatibility layer needs for each trait.
1919
public var deprecatedMembersByTrait: [String: DeprecatedMemberInfo] = [:]
2020

21-
/// Cache for `replacementChildren(for:by:)`. Ensures that we don't create two different replacement children even
22-
/// if we refactor the same child twice, so we can reliably equate and hash `Child` objects by object identity.
21+
/// Cache for `replacementChildren(for:by:historical:)`. Ensures that we don't create two different replacement
22+
/// children even if we refactor the same child twice, so we can reliably equate and hash `Child` objects by
23+
/// object identity.
2324
private var cachedReplacementChildren: [Child: [Child]] = [:]
2425

2526
/// Returns the deprecated members that the compatibility layer needs for `node`.
@@ -46,13 +47,23 @@ public struct CompatibilityLayer {
4647

4748
/// Returns the child or children that would have existed in place of this
4849
/// child before this refactoring was applied.
49-
private mutating func replacementChildren(for newerChild: Child, by refactoring: Child.Refactoring) -> [Child] {
50+
///
51+
/// - Parameters:
52+
/// - newerChild: The child which is being replaced.
53+
/// - refactoring: The refactoring which created that child and must be
54+
/// reversed.
55+
fileprivate mutating func replacementChildren(
56+
for newerChild: Child,
57+
by refactoring: Child.Refactoring,
58+
historical: Bool
59+
) -> [Child] {
5060
func make() -> [Child] {
5161
switch refactoring {
5262
case .renamed(from: let deprecatedName):
5363
return [
5464
Child(
5565
renamingTo: deprecatedName,
66+
makingHistorical: historical,
5667
newerChildPath: [newerChild]
5768
)
5869
]
@@ -72,8 +83,20 @@ public struct CompatibilityLayer {
7283
}
7384

7485
return newerGrandchildren.map { newerGrandchild in
75-
Child(newerChildPath: [newerChild, newerGrandchild])
86+
Child(
87+
makingHistorical: historical,
88+
newerChildPath: [newerChild, newerGrandchild]
89+
)
7690
}
91+
92+
case .introduced:
93+
return [
94+
Child(
95+
makingHistorical: historical,
96+
makingHidden: true,
97+
newerChildPath: [newerChild]
98+
)
99+
]
77100
}
78101
}
79102

@@ -100,6 +123,7 @@ public struct CompatibilityLayer {
100123
deprecatedMembersByNode[node.syntaxNodeKind] = result
101124
}
102125

126+
/// Compute and cache compatibility layer information for the given children.
103127
private mutating func computeMembers(for trait: Trait) {
104128
guard deprecatedMembersByTrait[trait.traitName] == nil else {
105129
return
@@ -115,58 +139,146 @@ public struct CompatibilityLayer {
115139
deprecatedMembersByTrait[trait.traitName] = result
116140
}
117141

118-
/// Compute and cache compatibility layer information for the given children.
142+
/// Compute compatibility layer information for the given children.
119143
private mutating func computeMembersFor(
120144
typeName: String,
121145
initialChildren: [Child],
122146
history: Child.History,
123147
areRequirements: Bool
124148
) -> DeprecatedMemberInfo {
125-
// The results that will ultimately be saved into the DeprecatedMemberInfo.
149+
var builder = DeprecatedMemberInfo.Builder(
150+
typeName: typeName,
151+
children: initialChildren,
152+
areRequirements: areRequirements
153+
)
154+
155+
// If any of the children are experimental, apply an initial change set that hides them, ensuring that we generate
156+
// APIs which aren't experimental.
157+
let experimentalChildren = initialChildren.filter { $0.isExperimental && !$0.isUnexpectedNodes }
158+
if !experimentalChildren.isEmpty {
159+
let syntheticChangeSet = experimentalChildren.map { ($0.name, Child.Refactoring.introduced) }
160+
builder.applyChangeSet(syntheticChangeSet, for: &self, historical: false)
161+
}
162+
163+
// Apply changes in the history
164+
for changeSet in history {
165+
builder.applyChangeSet(changeSet, for: &self, historical: true)
166+
}
167+
168+
return builder.make()
169+
}
170+
}
171+
172+
/// Describes the deprecated members of a given type that the compatibility layer ought to provide.
173+
public struct DeprecatedMemberInfo {
174+
/// Properties that are needed in the compatibility layer, in the order they ought to appear in the generated file.
175+
public var vars: [Child] = []
176+
177+
/// Initializer signatures that are needed in the compatibility layer, in the order they ought to appear in the generated file.
178+
public var inits: [InitSignature] = []
179+
180+
/// Is there anything whatsoever that we ought to generate?
181+
public var isEmpty: Bool {
182+
return vars.isEmpty && inits.isEmpty
183+
}
184+
185+
fileprivate struct Builder {
186+
/// Properties that are needed in the compatibility layer, in the order they ought to appear in the generated file.
187+
/// This becomes a property of the `DeprecatedMemberInfo`.
126188
var vars: [Child] = []
127-
var initSignatures: [InitSignature] = []
128189

129-
// Temporary working state for the loop.
130-
var children = initialChildren
131-
var knownVars = Set(children)
190+
/// Initializer signatures that are needed in the compatibility layer, in the order they ought to appear in the generated file.
191+
/// This becomes a property of the `DeprecatedMemberInfo`.
192+
var inits: [InitSignature] = []
132193

133-
func firstIndexOfChild(named targetName: String) -> Int {
134-
guard let i = children.firstIndex(where: { $0.name == targetName }) else {
135-
fatalError(
136-
"couldn't find '\(targetName)' in current children of \(typeName): \(String(reflecting: children.map(\.name)))"
137-
)
138-
}
139-
return i
194+
/// Name of the type we're generating a compatibility layer for.
195+
private let typeName: String
196+
197+
/// Are we building a compatibility layer for requirements of a trait? Traits don't have unexpected children or
198+
/// initializers.
199+
private let areRequirements: Bool
200+
201+
/// The current set of children after applying all of the change sets ever passed to `applyChangeSet(_:for:historical:)`.
202+
/// This is working state.
203+
private var children: [Child]
204+
205+
/// The set of all children that have ever been added to `vars`, plus the ones that were originally present.
206+
/// Used to ensure duplicates aren't added to `vars`. This is working state.
207+
private var knownVars: Set<Child>
208+
209+
/// Creates a builder with no deprecated members, but ready to start adding change sets.
210+
init(typeName: String, children: [Child], areRequirements: Bool) {
211+
self.typeName = typeName
212+
self.areRequirements = areRequirements
213+
214+
self.children = children
215+
self.knownVars = Set(children)
140216
}
141217

142-
for changeSet in history {
218+
/// Creates a `DeprecatedMemberInfo` from all the change sets that have been passed to
219+
/// `applyChangeSet(_:for:historical:)`.
220+
func make() -> DeprecatedMemberInfo {
221+
return DeprecatedMemberInfo(vars: vars, inits: inits)
222+
}
223+
224+
/// Generate the new `vars` and `inits` that are required to maintain compatibility with `changeSet`.
225+
///
226+
/// - Parameters:
227+
/// - changeSet: The changes to apply. This type is basically a generic form of `Child.ChangeSet`.
228+
/// - compatibilityLayer: The compatibility layer that these children will ultimately belong to.
229+
/// - historical: Should the children created by this change set be marked historical (and thus be deprecated)?
230+
mutating func applyChangeSet(
231+
_ changeSet: some RandomAccessCollection<(key: String, value: Child.Refactoring)>,
232+
for compatibilityLayer: inout CompatibilityLayer,
233+
historical: Bool
234+
) {
143235
var unexpectedChildrenWithNewNames: Set<Child> = []
144236

145237
// First pass: Apply the changes explicitly specified in the change set.
146238
for (currentName, refactoring) in changeSet {
147239
let i = firstIndexOfChild(named: currentName)
148240

149-
let replacementChildren = replacementChildren(for: children[i], by: refactoring)
241+
let replacementChildren = compatibilityLayer.replacementChildren(
242+
for: children[i],
243+
by: refactoring,
244+
historical: historical
245+
)
150246
children.replaceSubrange(i...i, with: replacementChildren)
151247

152248
if !areRequirements {
249+
func isDifferent(_ newChild: Child) -> Bool {
250+
newChild.isHidden || currentName != newChild.name
251+
}
252+
153253
// Mark adjacent unexpected node children whose names have changed too.
154-
if currentName != replacementChildren.first?.name {
254+
if let firstNewChild = replacementChildren.first, isDifferent(firstNewChild) {
155255
unexpectedChildrenWithNewNames.insert(children[i - 1])
156256
}
157-
if currentName != replacementChildren.last?.name {
257+
if let lastNewChild = replacementChildren.last, isDifferent(lastNewChild) {
158258
unexpectedChildrenWithNewNames.insert(children[i + replacementChildren.count])
159259
}
160260
}
161261
}
162262

163263
// Second pass: Update unexpected node children adjacent to those changes whose names have probably changed.
164-
for unexpectedChild in unexpectedChildrenWithNewNames {
264+
for unexpectedChild in unexpectedChildrenWithNewNames where !unexpectedChild.isHidden {
165265
precondition(unexpectedChild.isUnexpectedNodes)
166266
let i = firstIndexOfChild(named: unexpectedChild.name)
167267

168-
let earlier = children[checked: i - 1]
169-
let later = children[checked: i + 1]
268+
guard i == 0 || !children[i - 1].isHidden else {
269+
// Special case: `unexpectedChild` follows a hidden child and should be hidden too.
270+
children[i] = Child(makingHistorical: historical, makingHidden: true, newerChildPath: [unexpectedChild])
271+
continue
272+
}
273+
274+
// Find nearest expected, non-hidden node before `unexpectedChild`
275+
let allEarlier = children.prefix(through: max(i - 1, children.startIndex))
276+
let earlier = allEarlier.last { !$0.isHidden && !$0.isUnexpectedNodes }
277+
278+
// Find nearest expected, non-hidden node after `unexpectedChild`
279+
let allLater = children.suffix(from: min(i + 1, children.endIndex))
280+
let later = allLater.first { !$0.isHidden && !$0.isUnexpectedNodes }
281+
170282
precondition(!(earlier?.isUnexpectedNodes ?? false) && !(later?.isUnexpectedNodes ?? false))
171283

172284
let newChild = Child(forUnexpectedBetween: earlier, and: later, newerChildPath: [unexpectedChild])
@@ -176,33 +288,23 @@ public struct CompatibilityLayer {
176288
children[i] = newChild
177289
}
178290

179-
// 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.
180-
vars += children.filter { knownVars.insert($0).inserted }
291+
// Third pass: Append newly-created children to vars. We do this now so that changes from the first two passes
292+
// are properly interleaved, preserving source order.
293+
self.vars += children.filter { !$0.isHidden && knownVars.insert($0).inserted }
181294

182295
// We don't create compatibility layers for protocol requirement inits.
183296
if !areRequirements {
184-
initSignatures.append(InitSignature(children: children))
297+
self.inits.append(InitSignature(children: children))
185298
}
186299
}
187300

188-
return DeprecatedMemberInfo(vars: vars, inits: initSignatures)
189-
}
190-
}
191-
192-
/// Describes the deprecated members of a given type that the compatibility layer ought to provide.
193-
public struct DeprecatedMemberInfo {
194-
/// Properties that are needed in the compatibility layer, in the order they ought to appear in the generated file.
195-
public var vars: [Child] = []
196-
197-
/// Initializer signatures that are needed in the compatibility layer, in the order they ought to appear in the generated file.
198-
public var inits: [InitSignature] = []
199-
}
200-
201-
extension Array {
202-
/// Returns `nil` if `i` is out of bounds, or the indicated element otherwise.
203-
fileprivate subscript(checked i: Index) -> Element? {
204-
get {
205-
return indices.contains(i) ? self[i] : nil
301+
private func firstIndexOfChild(named targetName: String) -> Int {
302+
guard let i = children.firstIndex(where: { $0.name == targetName }) else {
303+
fatalError(
304+
"couldn't find '\(targetName)' in current children of \(typeName): \(String(reflecting: children.map(\.name)))"
305+
)
306+
}
307+
return i
206308
}
207309
}
208310
}

0 commit comments

Comments
 (0)