blob: 2f8981b8886b3a2fb30a326ce62cd78e87d152e2 [file] [log] [blame]
// Copyright 2025 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build !go1.25
package tokeninternal
import (
"cmp"
"fmt"
"go/token"
"slices"
"sync"
"sync/atomic"
"unsafe"
)
// AddExistingFiles adds the specified files to the FileSet if they
// are not already present. It panics if any pair of files in the
// resulting FileSet would overlap.
func AddExistingFiles(fset *token.FileSet, files []*token.File) {
// This function cannot be implemented as:
//
// for _, file := range files {
// if prev := fset.File(token.Pos(file.Base())); prev != nil {
// if prev != file {
// panic("FileSet contains a different file at the same base")
// }
// continue
// }
// file2 := fset.AddFile(file.Name(), file.Base(), file.Size())
// file2.SetLines(file.Lines())
// }
//
// because all calls to AddFile must be in increasing order.
// AddExistingFiles lets us augment an existing FileSet
// sequentially, so long as all sets of files have disjoint
// ranges.
// Punch through the FileSet encapsulation.
type tokenFileSet struct {
// This type remained essentially consistent from go1.16 to go1.21.
mutex sync.RWMutex
base int
files []*token.File
_ atomic.Pointer[token.File]
}
// If the size of token.FileSet changes, this will fail to compile.
const delta = int64(unsafe.Sizeof(tokenFileSet{})) - int64(unsafe.Sizeof(token.FileSet{}))
var _ [-delta * delta]int
type uP = unsafe.Pointer
var ptr *tokenFileSet
*(*uP)(uP(&ptr)) = uP(fset)
ptr.mutex.Lock()
defer ptr.mutex.Unlock()
cmp := func(x, y *token.File) int {
return cmp.Compare(x.Base(), y.Base())
}
// A naive implementation would simply concatenate and sort
// the arrays. However, the typical usage pattern is to
// repeatedly add a handful of files to a large FileSet, which
// would cause O(n) sort operations each of O(n log n), where
// n is the total size.
//
// A more efficient approach is to sort the new items, then
// merge the two sorted lists. Although it is possible to do
// this in-place with only constant additional space, it is
// quite fiddly; see "Practical In-Place Merging", Huang &
// Langston, CACM, 1988.
// (https://dl.acm.org/doi/pdf/10.1145/42392.42403)
//
// If we could change the representation of FileSet, we could
// use double-buffering: allocate a second array of size m+n
// into which we merge the m initial items and the n new ones,
// then we switch the two arrays until the next time.
//
// But since we cannot, for now we grow the existing array by
// doubling to at least 2*(m+n), use the upper half as
// temporary space, then copy back into the lower half.
// Any excess capacity will help amortize future calls to
// AddExistingFiles.
//
// The implementation of FileSet for go1.25 is expected to use
// a balanced tree, making FileSet.AddExistingFiles much more
// efficient; see CL 675736.
m, n := len(ptr.files), len(files)
size := m + n // final size assuming no duplicates
ptr.files = slices.Grow(ptr.files, 2*size)
ptr.files = append(ptr.files, files...)
// Sort the new files, without mutating the files argument.
// (The existing ptr.files are already sorted.)
slices.SortFunc(ptr.files[m:size], cmp)
// Merge old (x) and new (y) files into output array.
// For simplicity, we remove dups and check overlaps as a second pass.
var (
x, y, out = ptr.files[:m], ptr.files[m:size], ptr.files[size:size]
xi, yi = 0, 0
)
for xi < m && yi < n {
xf := x[xi]
yf := y[yi]
switch cmp(xf, yf) {
case -1:
out = append(out, xf)
xi++
case +1:
out = append(out, yf)
yi++
default:
yi++ // equal; discard y
}
}
out = append(out, x[xi:]...)
out = append(out, y[yi:]...)
// Compact out into start of ptr.files array,
// rejecting overlapping files and
// discarding adjacent identical files.
ptr.files = ptr.files[:0]
for i, file := range out {
if i > 0 {
prev := out[i-1]
if file == prev {
continue
}
if prev.Base()+prev.Size()+1 > file.Base() {
panic(fmt.Sprintf("file %s (%d-%d) overlaps with file %s (%d-%d)",
prev.Name(), prev.Base(), prev.Base()+prev.Size(),
file.Name(), file.Base(), file.Base()+file.Size()))
}
}
ptr.files = append(ptr.files, file)
}
// This ensures that we don't keep a File alive after RemoveFile.
clear(ptr.files[size:cap(ptr.files)])
// Advance FileSet.Base().
if len(ptr.files) > 0 {
last := ptr.files[len(ptr.files)-1]
newBase := last.Base() + last.Size() + 1
if ptr.base < newBase {
ptr.base = newBase
}
}
}