Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 75 additions & 2 deletions Sources/SwiftParser/CodeParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ public final class CodeParser {
private let tokenizer: CodeTokenizer
private var expressionBuilders: [CodeExpressionBuilder]

// State for incremental parsing
private var lastContext: CodeContext?
private var snapshots: [Int: CodeContext.Snapshot] = [:]
private var lastTokens: [any CodeToken] = []

public init(tokenizer: CodeTokenizer, builders: [CodeElementBuilder] = [], expressionBuilders: [CodeExpressionBuilder] = []) {
self.tokenizer = tokenizer
self.builders = builders
Expand All @@ -26,7 +31,12 @@ public final class CodeParser {
public func parse(_ input: String, rootNode: CodeNode) -> (node: CodeNode, context: CodeContext) {
let tokens = tokenizer.tokenize(input)
var context = CodeContext(tokens: tokens, index: 0, currentNode: rootNode, errors: [], input: input)

snapshots = [:]
lastTokens = tokens

while context.index < context.tokens.count {
snapshots[context.index] = context.snapshot()
let token = context.tokens[context.index]
if token.kindDescription == "eof" {
break
Expand Down Expand Up @@ -55,12 +65,75 @@ public final class CodeParser {
context.index += 1
}
}
snapshots[context.index] = context.snapshot()
lastContext = context
return (rootNode, context)
}

public func update(_ input: String, rootNode: CodeNode) -> (node: CodeNode, context: CodeContext) {
// Simple implementation: reparse everything
return parse(input, rootNode: rootNode)
guard var context = lastContext else {
return parse(input, rootNode: rootNode)
}

let newTokens = tokenizer.tokenize(input)

var diffIndex = 0
while diffIndex < min(lastTokens.count, newTokens.count) {
if !tokenEqual(lastTokens[diffIndex], newTokens[diffIndex]) {
break
}
diffIndex += 1
}

var restoreIndex = diffIndex
while restoreIndex >= 0 && snapshots[restoreIndex] == nil {
restoreIndex -= 1
}
if let snap = snapshots[restoreIndex] {
context.restore(snap)
}

context.tokens = newTokens
context.index = restoreIndex

snapshots = snapshots.filter { $0.key <= restoreIndex }
lastTokens = newTokens

while context.index < context.tokens.count {
snapshots[context.index] = context.snapshot()
let token = context.tokens[context.index]
if token.kindDescription == "eof" { break }
var matched = false
for builder in builders {
if builder.accept(context: context, token: token) {
builder.build(context: &context)
matched = true
break
}
}
if !matched {
for expr in expressionBuilders {
if expr.accept(context: context, token: token) {
if let node = expr.parse(context: &context) {
context.currentNode.addChild(node)
}
matched = true
break
}
}
}
if !matched {
context.errors.append(CodeError("Unrecognized token \(token.kindDescription)", range: token.range))
context.index += 1
}
}
snapshots[context.index] = context.snapshot()
lastContext = context
return (rootNode, context)
}

private func tokenEqual(_ a: any CodeToken, _ b: any CodeToken) -> Bool {
return a.kindDescription == b.kindDescription && a.text == b.text
}

public func parseExpression(context: inout CodeContext, minBP: Int = 0) -> CodeNode? {
Expand Down
10 changes: 10 additions & 0 deletions Tests/SwiftParserTests/SwiftParserTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,14 @@ final class SwiftParserTests: XCTestCase {
XCTAssertEqual(ctx.errors.count, 0)
XCTAssertEqual(root.children.count, 0)
}

func testIncrementalUpdateRollback() {
let lang = PythonLanguage()
let parser = CodeParser(tokenizer: lang.tokenizer, builders: lang.builders, expressionBuilders: lang.expressionBuilders)
let root = CodeNode(type: lang.rootElement, value: "")
_ = parser.parse("x = 1", rootNode: root)
XCTAssertEqual(root.children.first?.children.first?.value, "1")
_ = parser.update("x = 2", rootNode: root)
XCTAssertEqual(root.children.first?.children.first?.value, "2")
}
}