Skip to content

Commit c044d1d

Browse files
committed
[SwiftParser] Intern the whole source buffer for a full parse
748dde4 ([SwiftParser] Intern source text per parsed token) changed parsing to intern each token's text individually instead of copying the whole source buffer once, so a parsed tree no longer retains the source buffer. That is important for incremental reparsing memory, but it regressed full-parse performance: a full parse now copies the source in as many pieces as there are tokens, each with its own bump-allocation and memcpy, rather than in a single bulk copy. For a full (non-incremental) parse, copy the whole source into the arena once and lex over that copy. Parsed tokens then point directly into arena-owned memory, so `internParsedTokenText` returns their text unchanged instead of copying it, and the tree stays self-contained. An incremental reparse keeps interning each re-lexed token individually, so its new tree still references only the re-lexed spans and not the whole buffer. `ParsingRawSyntaxArena.internSourceBuffer` copies the buffer, records its bounds, and returns the copy for `Parser` to lex over on a full parse; `internParsedTokenText` returns text that lies within the copy unchanged and copies anything else (e.g. synthesized tokens). Because the copy is owned by the arena, a full parse needs no parser-owned source copy, and nothing points into the caller's buffer once parsing returns.
1 parent b8e7d60 commit c044d1d

2 files changed

Lines changed: 54 additions & 12 deletions

File tree

Sources/SwiftParser/Parser.swift

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -239,20 +239,23 @@ public struct Parser {
239239
self.arena = arena ?? ParsingRawSyntaxArena(parseTriviaFunction: TriviaParser.parseTrivia)
240240

241241
var input = input
242-
if copySource {
243-
// Copy the source into a parser-owned buffer so the caller may free their
244-
// buffer once this initializer returns. The buffer is freed when this
245-
// `Parser` is destroyed (i.e. when the parse call returns); each token's
246-
// text is interned into the arena at node-creation time, so the resulting
247-
// tree does not reference this buffer.
242+
if parseTransition == nil {
243+
// Full parse: copy the whole source into the arena and lex over that copy,
244+
// so parsed tokens point into arena-owned memory and need no per-token
245+
// interning.
246+
input = self.arena.internSourceBuffer(input)
247+
self.sourceBufferOwner = nil
248+
} else if copySource {
249+
// Incremental reparse over a buffer that will not outlive this
250+
// initializer: keep a parser-owned copy, freed when this `Parser` is
251+
// destroyed. Each re-lexed token's text is interned into the arena.
248252
let owner = ParserSourceBufferOwner(copying: input)
249253
self.sourceBufferOwner = owner
250254
input = owner.buffer
251255
} else {
252-
// No-copy: lex directly over the caller-provided buffer. The caller
253-
// guarantees it stays valid for the entire parse (see `withParser`). Each
254-
// token's text is interned into the arena, so the tree is self-contained
255-
// and does not reference the buffer after parsing.
256+
// Incremental reparse over the caller-provided buffer, which the caller
257+
// guarantees stays valid for the parse (see `withParser`). Each re-lexed
258+
// token's text is interned into the arena.
256259
self.sourceBufferOwner = nil
257260
}
258261

Sources/SwiftSyntax/Raw/RawSyntaxArena.swift

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -202,18 +202,57 @@ public class ParsingRawSyntaxArena: RawSyntaxArena {
202202
/// - Important: Must never be changed to a mutable value. See `RawSyntaxArenaRef.parseTrivia`.
203203
private let parseTriviaFunction: ParseTriviaFunction
204204

205+
/// The arena's own copy of the whole source buffer, made by
206+
/// `internSourceBuffer` for a full parse. `nil` for an incremental reparse,
207+
/// which does not copy the whole buffer. When set, the lexer runs over this
208+
/// copy, so a parsed token whose text lies within it is already arena-owned
209+
/// and `internParsedTokenText` returns it unchanged.
210+
private struct SourceCopy {
211+
let start: UnsafePointer<UInt8>
212+
let end: UnsafePointer<UInt8>
213+
}
214+
private var sourceCopy: SourceCopy?
215+
205216
public init(parseTriviaFunction: @escaping ParseTriviaFunction) {
206217
self.parseTriviaFunction = parseTriviaFunction
207218
super.init(slabSize: 4096)
208219
}
209220

221+
/// Copy the whole source buffer into this arena and return the copy, so the
222+
/// caller can lex over arena-owned memory. Parsed tokens then point directly
223+
/// into the copy and need no per-token interning, and the resulting tree is
224+
/// self-contained without referencing the caller's buffer.
225+
///
226+
/// This suits a full parse, where every token's text is needed and the tree
227+
/// legitimately retains text the size of the source. It must not be used for
228+
/// an incremental reparse, where copying the whole buffer would defeat the
229+
/// point of reusing nodes; there, `internParsedTokenText` copies each
230+
/// re-lexed token individually instead.
231+
@_spi(RawSyntax) public func internSourceBuffer(_ buffer: UnsafeBufferPointer<UInt8>) -> UnsafeBufferPointer<UInt8> {
232+
guard buffer.count > 0 else {
233+
self.sourceCopy = nil
234+
return buffer
235+
}
236+
let dest = allocateTextBuffer(count: buffer.count)
237+
_ = dest.initialize(from: buffer)
238+
let copyBase = UnsafePointer(dest.baseAddress!)
239+
self.sourceCopy = SourceCopy(start: copyBase, end: copyBase + buffer.count)
240+
return UnsafeBufferPointer(start: copyBase, count: buffer.count)
241+
}
242+
210243
/// Intern a parsed token's whole text into the arena's node allocator so the
211244
/// resulting node does not depend on the source buffer.
212245
///
213246
/// Unlike `intern(_:)`, this skips the `contains` check: lexer-produced text
214-
/// is never already managed by the arena, so a copy is always needed.
247+
/// is never already managed by the arena, so a copy is normally needed. When
248+
/// the source was copied into the arena up front (`internSourceBuffer`, i.e. a
249+
/// full parse), a token that lies within that copy is already arena-owned and
250+
/// is returned unchanged.
215251
func internParsedTokenText(_ text: SyntaxText) -> SyntaxText {
216-
if text.isEmpty {
252+
guard let base = text.baseAddress, !text.isEmpty else {
253+
return text
254+
}
255+
if let sourceCopy, base >= sourceCopy.start, base + text.count <= sourceCopy.end {
217256
return text
218257
}
219258
let allocated = allocateTextBuffer(count: text.count)

0 commit comments

Comments
 (0)