Skip to content

Commit 0055ebe

Browse files
committed
Register affect range in lookahead
This change will prevent us from skipping potential parts of a node when parsing incrementally
1 parent ae8283c commit 0055ebe

File tree

5 files changed

+103
-22
lines changed

5 files changed

+103
-22
lines changed

Sources/SwiftParser/Lookahead.swift

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,22 +27,29 @@ extension Parser {
2727
/// i.e. how far it looked ahead.
2828
var tokensConsumed: Int = 0
2929

30+
var parseTransition: IncrementalParseTransition?
31+
/// Initialize the offset to the start of source
32+
var currentOffset: Int
33+
3034
private init(
3135
lexemes: Lexer.LexemeSequence,
32-
currentToken: Lexer.Lexeme
36+
currentToken: Lexer.Lexeme,
37+
parseTransition: IncrementalParseTransition? = nil
3338
) {
3439
self.lexemes = lexemes
3540
self.currentToken = currentToken
41+
self.parseTransition = parseTransition
42+
self.currentOffset = lexemes.getOffsetToStart(self.currentToken)
3643
}
3744

3845
fileprivate init(cloning other: Parser) {
39-
self.init(lexemes: other.lexemes, currentToken: other.currentToken)
46+
self.init(lexemes: other.lexemes, currentToken: other.currentToken, parseTransition: other.parseTransition)
4047
}
4148

4249
/// Initiates a lookahead session from the current point in this
4350
/// lookahead session.
4451
func lookahead() -> Lookahead {
45-
return Lookahead(lexemes: self.lexemes, currentToken: self.currentToken)
52+
return Lookahead(lexemes: self.lexemes, currentToken: self.currentToken, parseTransition: self.parseTransition)
4653
}
4754
}
4855

@@ -89,6 +96,7 @@ extension Parser.Lookahead {
8996

9097
mutating func consumeAnyToken() {
9198
tokensConsumed += 1
99+
registerAffectRange()
92100
self.currentToken = self.lexemes.advance()
93101
}
94102

@@ -399,4 +407,10 @@ extension Parser.Lookahead {
399407
}
400408
}
401409
}
410+
411+
fileprivate mutating func registerAffectRange() {
412+
self.parseTransition?.registerAffectRange(at: self.currentOffset, length: self.currentToken.byteLength)
413+
currentOffset += self.currentToken.byteLength
414+
self.parseTransition?.registerAffectRange(at: self.currentOffset, length: self.lexemes.peek().byteLength)
415+
}
402416
}

Sources/SwiftParser/Parser.swift

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,12 @@ public struct Parser {
145145
/// arena is created automatically, and `input` copied into the
146146
/// arena. If non-`nil`, `input` must be within its registered
147147
/// source buffer or allocator.
148-
public init(_ input: UnsafeBufferPointer<UInt8>, maximumNestingLevel: Int? = nil, arena: ParsingSyntaxArena? = nil, parseTransition: IncrementalParseTransition? = nil) {
148+
public init(
149+
_ input: UnsafeBufferPointer<UInt8>,
150+
maximumNestingLevel: Int? = nil,
151+
arena: ParsingSyntaxArena? = nil,
152+
parseTransition: IncrementalParseTransition? = nil
153+
) {
149154
self.maximumNestingLevel = maximumNestingLevel ?? Self.defaultMaximumNestingLevel
150155

151156
var sourceBuffer: UnsafeBufferPointer<UInt8>
@@ -629,7 +634,11 @@ extension Parser {
629634
// MARK: Incremental Parsing
630635
extension Parser {
631636
mutating func loadCurrentSyntaxNodeFromCache(for kind: SyntaxKind) -> Syntax? {
632-
guard let parseTransition = self.parseTransition else { return nil }
637+
guard let parseTransition = self.parseTransition,
638+
parseTransition.isValidTransition()
639+
else {
640+
return nil
641+
}
633642

634643
var lookUpHelper = IncrementalParseLookup(transition: parseTransition)
635644
let currentOffset = self.lexemes.getOffsetToStart(self.currentToken)

Sources/SwiftSyntax/IncrementalParseTransition.swift

Lines changed: 55 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,14 @@ public final class IncrementalParseReusedNodeCollector:
4545
/// Keeps track of a previously parsed syntax tree and the source edits that
4646
/// occurred since it was created.
4747
public final class IncrementalParseTransition {
48-
fileprivate let previousTree: SourceFileSyntax
49-
fileprivate let edits: ConcurrentEdits
50-
fileprivate let reusedDelegate: IncrementalParseReusedNodeDelegate?
48+
fileprivate var previousTree: SourceFileSyntax?
49+
fileprivate var edits: ConcurrentEdits?
50+
fileprivate var reusedDelegate: IncrementalParseReusedNodeDelegate?
51+
52+
fileprivate var previousLookaheadRange: [Int: Int] = [:]
53+
/// Keep track of how far we would look when calling ``Lookahead``
54+
/// Key is offset to buffer start and value is the length of we lookahead
55+
fileprivate var cursorLookaheadRange: [Int: Int] = [:]
5156

5257
/// - Parameters:
5358
/// - previousTree: The previous tree to do lookups on.
@@ -56,14 +61,30 @@ public final class IncrementalParseTransition {
5661
/// - reusedNodeDelegate: Optional delegate to accept information about the
5762
/// reused regions and nodes.
5863
public init(
59-
previousTree: SourceFileSyntax,
60-
edits: ConcurrentEdits,
64+
previousTree: SourceFileSyntax? = nil,
65+
edits: ConcurrentEdits? = nil,
6166
reusedNodeDelegate: IncrementalParseReusedNodeDelegate? = nil
6267
) {
6368
self.previousTree = previousTree
6469
self.edits = edits
6570
self.reusedDelegate = reusedNodeDelegate
6671
}
72+
73+
public func setupTransition(tree: SourceFileSyntax, edits: ConcurrentEdits, delegate: IncrementalParseReusedNodeDelegate? = nil) {
74+
self.previousTree = tree
75+
self.edits = edits
76+
self.reusedDelegate = delegate
77+
self.previousLookaheadRange = cursorLookaheadRange
78+
self.cursorLookaheadRange = [:]
79+
}
80+
81+
public func registerAffectRange(at offset: Int, length: Int) {
82+
self.cursorLookaheadRange[offset] = length
83+
}
84+
85+
public func isValidTransition() -> Bool {
86+
return previousTree != nil
87+
}
6788
}
6889

6990
fileprivate extension Sequence where Element: Comparable {
@@ -204,16 +225,14 @@ public struct ConcurrentEdits {
204225
/// updated source that was already parsed during a previous parse invocation.
205226
public struct IncrementalParseLookup {
206227
fileprivate let transition: IncrementalParseTransition
207-
fileprivate var cursor: SyntaxCursor
208228

209229
/// Create a new ``IncrementalParseLookup`` that can look nodes up based on the
210230
/// given ``IncrementalParseTransition``.
211231
public init(transition: IncrementalParseTransition) {
212232
self.transition = transition
213-
self.cursor = .init(root: transition.previousTree.data)
214233
}
215234

216-
fileprivate var edits: ConcurrentEdits {
235+
fileprivate var edits: ConcurrentEdits? {
217236
return transition.edits
218237
}
219238

@@ -242,7 +261,7 @@ public struct IncrementalParseLookup {
242261
let node = cursorLookup(prevPosition: prevPosition, kind: kind)
243262
if let delegate = reusedDelegate, let node {
244263
delegate.parserReusedNode(
245-
range: ByteSourceRange(offset: newOffset, length: node.byteSizeAfterTrimmingTrivia),
264+
range: ByteSourceRange(offset: node.positionAfterSkippingLeadingTrivia.utf8Offset, length: node.byteSizeAfterTrimmingTrivia),
246265
previousNode: node
247266
)
248267
}
@@ -253,20 +272,28 @@ public struct IncrementalParseLookup {
253272
prevPosition: AbsolutePosition,
254273
kind: SyntaxKind
255274
) -> Syntax? {
275+
guard let data = transition.previousTree?.data else {
276+
return nil
277+
}
278+
279+
var cursor = SyntaxCursor(root: data)
256280
guard !cursor.finished else { return nil }
257281

258282
while true {
259-
if nodeAtCursorCanBeReused(prevPosition: prevPosition, kind: kind) {
283+
if nodeAtCursorCanBeReused(cursor, prevPosition: prevPosition, kind: kind) {
260284
return cursor.asSyntaxNode
261285
}
262286
guard cursor.advanceToNextNode(at: prevPosition) else { return nil }
263287
}
264288
}
265289

266290
fileprivate func nodeAtCursorCanBeReused(
291+
_ cursor: SyntaxCursor,
267292
prevPosition: AbsolutePosition,
268293
kind: SyntaxKind
269294
) -> Bool {
295+
guard let edits = edits else { return false }
296+
270297
let node = cursor.node
271298
if node.position != prevPosition {
272299
return false
@@ -298,7 +325,7 @@ public struct IncrementalParseLookup {
298325
}
299326
let nodeAffectRange = ByteSourceRange(
300327
offset: node.position.utf8Offset,
301-
length: (node.raw.totalLength + nextLeafNodeLength).utf8Length
328+
length: max(mergeLookaheadRange(at: node.position.utf8Offset, length: node.raw.totalLength.utf8Length), (node.raw.totalLength + nextLeafNodeLength).utf8Length)
302329
)
303330

304331
for edit in edits.edits {
@@ -317,6 +344,8 @@ public struct IncrementalParseLookup {
317344
}
318345

319346
fileprivate func translateToPreEditOffset(_ postEditOffset: Int) -> Int? {
347+
guard let edits = edits else { return nil }
348+
320349
var offset = postEditOffset
321350
for edit in edits.edits {
322351
if edit.range.offset > offset {
@@ -332,6 +361,21 @@ public struct IncrementalParseLookup {
332361
}
333362
return offset
334363
}
364+
365+
fileprivate func mergeLookaheadRange(at start: Int, length: Int) -> Int {
366+
var totalLength = start + length
367+
368+
let targetRanges = transition.previousLookaheadRange.filter { $0.key >= totalLength }.sorted(by: { $0.key < $1.key })
369+
370+
for targetRange in targetRanges {
371+
if targetRange.key != totalLength {
372+
break
373+
}
374+
totalLength += targetRange.value
375+
}
376+
377+
return totalLength
378+
}
335379
}
336380

337381
/// Functions as an iterator that walks the tree looking for nodes with a

Sources/_SwiftSyntaxTestSupport/IncrementalParseTestUtils.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,11 @@ public func assertIncrementalParse(
3030
let originalString = String(originalSource)
3131
let editedString = String(editedSource)
3232

33-
let originalTree = Parser.parse(source: originalString)
33+
let transition = IncrementalParseTransition()
34+
let originalTree = Parser.parse(source: originalString, parseTransition: transition)
3435

3536
let reusedNodesCollector = IncrementalParseReusedNodeCollector()
36-
let transition = IncrementalParseTransition(previousTree: originalTree, edits: concurrentEdits, reusedNodeDelegate: reusedNodesCollector)
37+
transition.setupTransition(tree: originalTree, edits: concurrentEdits, delegate: reusedNodesCollector)
3738

3839
let newTree = Parser.parse(source: editedString)
3940
let incrementallyParsedNewTree = Parser.parse(source: editedString, parseTransition: transition)

Tests/SwiftParserTest/IncrementalParsingTests.swift

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,7 @@ public class IncrementalParsingTests: XCTestCase {
2525
)
2626
}
2727

28-
public func testReusedNode() throws {
29-
try XCTSkipIf(true, "Swift parser does not handle node reuse yet")
28+
public func testReusedNode() {
3029
assertIncrementalParse(
3130
"""
3231
struct A⏩️⏸️A⏪️ {}
@@ -37,9 +36,8 @@ public class IncrementalParsingTests: XCTestCase {
3736
]
3837
)
3938
}
40-
39+
4140
public func testTrailingClosure() {
42-
XCTExpectFailure("WIP: Add lookahead for incremental parse")
4341
assertIncrementalParse(
4442
"""
4543
foo() {}
@@ -49,4 +47,19 @@ public class IncrementalParsingTests: XCTestCase {
4947
"""
5048
)
5149
}
50+
51+
public func testMultiFunctionCall() {
52+
assertIncrementalParse(
53+
"""
54+
foo() {}
55+
foo1() {}
56+
foo2() {}
57+
⏩️⏸️foo3() {}⏪️
58+
""",
59+
reusedNodes: [
60+
ReusedNodeSpec("foo() {}", kind: .codeBlockItem),
61+
ReusedNodeSpec("foo1() {}", kind: .codeBlockItem),
62+
]
63+
)
64+
}
5265
}

0 commit comments

Comments
 (0)