Skip to content

Commit 748dde4

Browse files
committed
[SwiftParser] Intern source text per parsed token
Previously, parsing interned the entire source buffer into the `RawSyntaxArena`, so a parsed token's `wholeText` pointed into it and the whole buffer stayed resident for the lifetime of the tree. For incremental reparsing this is wasteful: each reparse allocates a fresh arena that copies the full source again, even though most nodes are reused from the previous tree. Instead, intern each parsed token's text into the node allocator at token creation. The resulting tree is self-contained and no longer references the source buffer, so the buffer only needs to stay valid for the duration of the parse. The static `parse`/`parseIncrementally` methods, string-interpolation literal construction, `NameMatcher`, and the plugin macro-expansion host now lex directly over the caller's buffer via the new no-copy `Parser.withParser` entry points, copying nothing. The `Parser(_:)` initializers, whose parse happens after the initializer returns, instead keep a `Parser`-owned copy of the source that is freed when the `Parser` is destroyed. The source buffer is no longer null-terminated; all lexer reads are bounds-checked against the buffer length, so the trailing NUL was unnecessary. Add `testParseBufferEOFEdgeCases` covering EOF-adjacent lexing (unterminated comments/strings, truncated UTF-8, etc.) over exactly-sized allocations so the absence of the terminator is verified under AddressSanitizer.
1 parent a8b1c53 commit 748dde4

8 files changed

Lines changed: 287 additions & 97 deletions

File tree

Sources/SwiftCompilerPluginMessageHandling/PluginMacroExpansionContext.swift

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -47,26 +47,29 @@ class ParsedSyntaxRegistry {
4747
swiftVersion: Parser.SwiftVersion,
4848
experimentalFeatures: Parser.ExperimentalFeatures
4949
) -> Syntax {
50-
var parser = Parser(
51-
source,
50+
// The parse runs entirely within `withParser`, so lex directly over the
51+
// source without copying it into a parser-owned buffer.
52+
return Parser.withParser(
53+
source: source,
5254
swiftVersion: swiftVersion,
5355
experimentalFeatures: experimentalFeatures
54-
)
55-
switch kind {
56-
case .declaration:
57-
return Syntax(DeclSyntax.parse(from: &parser))
58-
case .statement:
59-
return Syntax(StmtSyntax.parse(from: &parser))
60-
case .expression:
61-
return Syntax(ExprSyntax.parse(from: &parser))
62-
case .type:
63-
return Syntax(TypeSyntax.parse(from: &parser))
64-
case .pattern:
65-
return Syntax(PatternSyntax.parse(from: &parser))
66-
case .attribute:
67-
return Syntax(AttributeSyntax.parse(from: &parser))
68-
case .accessor:
69-
return Syntax(AccessorDeclSyntax.parse(from: &parser))
56+
) { parser in
57+
switch kind {
58+
case .declaration:
59+
return Syntax(DeclSyntax.parse(from: &parser))
60+
case .statement:
61+
return Syntax(StmtSyntax.parse(from: &parser))
62+
case .expression:
63+
return Syntax(ExprSyntax.parse(from: &parser))
64+
case .type:
65+
return Syntax(TypeSyntax.parse(from: &parser))
66+
case .pattern:
67+
return Syntax(PatternSyntax.parse(from: &parser))
68+
case .attribute:
69+
return Syntax(AttributeSyntax.parse(from: &parser))
70+
case .accessor:
71+
return Syntax(AccessorDeclSyntax.parse(from: &parser))
72+
}
7073
}
7174
}
7275

Sources/SwiftIDEUtils/NameMatcher.swift

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@
1111
//===----------------------------------------------------------------------===//
1212

1313
#if compiler(>=6)
14-
import SwiftParser
14+
@_spi(RawSyntax) import SwiftParser
1515
public import SwiftSyntax
1616
#else
17-
import SwiftParser
17+
@_spi(RawSyntax) import SwiftParser
1818
import SwiftSyntax
1919
#endif
2020

@@ -227,8 +227,9 @@ public class NameMatcher: SyntaxAnyVisitor {
227227
let triviaRangeEndOffsetInToken = triviaRange.upperBound.utf8Offset - token.position.utf8Offset
228228
let commentTree = token.syntaxTextBytes[positionOffsetInToken..<triviaRangeEndOffsetInToken]
229229
.withUnsafeBufferPointer { (buffer) -> ExprSyntax in
230-
var parser = Parser(buffer)
231-
return ExprSyntax.parse(from: &parser)
230+
// The parse runs entirely within this scope, so lex directly over
231+
// `buffer` without copying it.
232+
Parser.withParser(source: buffer) { ExprSyntax.parse(from: &$0) }
232233
}
233234
// Run a new `NameMatcher`. Since the input of that name matcher is the text after the position to resolve, we
234235
// want to resolve the position at offset 0.

Sources/SwiftParser/ParseSourceFile.swift

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,13 @@ extension Parser {
2222
public static func parse(
2323
source: String
2424
) -> SourceFileSyntax {
25-
var parser = Parser(source)
26-
return SourceFileSyntax.parse(from: &parser)
25+
return withParser(
26+
source: source,
27+
maximumNestingLevel: nil,
28+
parseTransition: nil,
29+
swiftVersion: nil,
30+
experimentalFeatures: []
31+
) { SourceFileSyntax.parse(from: &$0) }
2732
}
2833

2934
/// A compiler interface that allows the enabling of experimental features.
@@ -33,8 +38,13 @@ extension Parser {
3338
swiftVersion: SwiftVersion? = nil,
3439
experimentalFeatures: ExperimentalFeatures
3540
) -> SourceFileSyntax {
36-
var parser = Parser(source, swiftVersion: swiftVersion, experimentalFeatures: experimentalFeatures)
37-
return SourceFileSyntax.parse(from: &parser)
41+
return withParser(
42+
source: source,
43+
maximumNestingLevel: nil,
44+
parseTransition: nil,
45+
swiftVersion: swiftVersion,
46+
experimentalFeatures: experimentalFeatures
47+
) { SourceFileSyntax.parse(from: &$0) }
3848
}
3949

4050
/// Parse the source code in the given buffer as Swift source file. See
@@ -44,8 +54,13 @@ extension Parser {
4454
maximumNestingLevel: Int? = nil,
4555
swiftVersion: SwiftVersion? = nil
4656
) -> SourceFileSyntax {
47-
var parser = Parser(source, maximumNestingLevel: maximumNestingLevel, swiftVersion: swiftVersion)
48-
return SourceFileSyntax.parse(from: &parser)
57+
return withParser(
58+
source: source,
59+
maximumNestingLevel: maximumNestingLevel,
60+
parseTransition: nil,
61+
swiftVersion: swiftVersion,
62+
experimentalFeatures: []
63+
) { SourceFileSyntax.parse(from: &$0) }
4964
}
5065

5166
/// Parse the source code in the given string as Swift source file with support
@@ -120,8 +135,13 @@ extension Parser {
120135
source: String,
121136
parseTransition: IncrementalParseTransition?
122137
) -> IncrementalParseResult {
123-
var parser = Parser(source, parseTransition: parseTransition)
124-
return IncrementalParseResult(tree: SourceFileSyntax.parse(from: &parser), lookaheadRanges: parser.lookaheadRanges)
138+
return withParser(
139+
source: source,
140+
maximumNestingLevel: nil,
141+
parseTransition: parseTransition,
142+
swiftVersion: nil,
143+
experimentalFeatures: []
144+
) { IncrementalParseResult(tree: SourceFileSyntax.parse(from: &$0), lookaheadRanges: $0.lookaheadRanges) }
125145
}
126146

127147
/// Parse the source code in the given buffer as Swift source file with support
@@ -133,8 +153,13 @@ extension Parser {
133153
maximumNestingLevel: Int? = nil,
134154
parseTransition: IncrementalParseTransition?
135155
) -> IncrementalParseResult {
136-
var parser = Parser(source, maximumNestingLevel: maximumNestingLevel, parseTransition: parseTransition)
137-
return IncrementalParseResult(tree: SourceFileSyntax.parse(from: &parser), lookaheadRanges: parser.lookaheadRanges)
156+
return withParser(
157+
source: source,
158+
maximumNestingLevel: maximumNestingLevel,
159+
parseTransition: parseTransition,
160+
swiftVersion: nil,
161+
experimentalFeatures: []
162+
) { IncrementalParseResult(tree: SourceFileSyntax.parse(from: &$0), lookaheadRanges: $0.lookaheadRanges) }
138163
}
139164
}
140165

0 commit comments

Comments
 (0)