Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
17 changes: 15 additions & 2 deletions internal/compiler/checkerpool.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ import (

type CheckerPool interface {
GetChecker(ctx context.Context) (*checker.Checker, func())
GetCheckerForFile(ctx context.Context, file *ast.SourceFile) (*checker.Checker, func())
GetCheckerForFileNonexclusive(ctx context.Context, file *ast.SourceFile) (*checker.Checker, func())
GetCheckerForFileExclusive(ctx context.Context, file *ast.SourceFile) (*checker.Checker, func())
GetAllCheckers(ctx context.Context) ([]*checker.Checker, func())
Files(checker *checker.Checker) iter.Seq[*ast.SourceFile]
}
Expand All @@ -24,6 +25,7 @@ type checkerPool struct {

createCheckersOnce sync.Once
checkers []*checker.Checker
locks []sync.Mutex
fileAssociations map[*ast.SourceFile]*checker.Checker
}

Expand All @@ -34,17 +36,28 @@ func newCheckerPool(checkerCount int, program *Program) *checkerPool {
program: program,
checkerCount: checkerCount,
checkers: make([]*checker.Checker, checkerCount),
locks: make([]sync.Mutex, checkerCount),
}

return pool
}

func (p *checkerPool) GetCheckerForFile(ctx context.Context, file *ast.SourceFile) (*checker.Checker, func()) {
func (p *checkerPool) GetCheckerForFileNonexclusive(ctx context.Context, file *ast.SourceFile) (*checker.Checker, func()) {
p.createCheckers()
checker := p.fileAssociations[file]
return checker, noop
}

func (p *checkerPool) GetCheckerForFileExclusive(ctx context.Context, file *ast.SourceFile) (*checker.Checker, func()) {
c, done := p.GetCheckerForFileNonexclusive(ctx, file)
idx := slices.Index(p.checkers, c)
Copy link

Copilot AI Nov 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing error handling for when slices.Index returns -1 (checker not found). If the checker is not in the slice, this will cause a panic when accessing p.locks[idx]. Although this should never happen in normal operation, defensive programming suggests adding a check or assertion.

Suggested change
idx := slices.Index(p.checkers, c)
idx := slices.Index(p.checkers, c)
if idx == -1 {
panic("checker not found in checker pool")
}

Copilot uses AI. Check for mistakes.
p.locks[idx].Lock()
return c, sync.OnceFunc(func() {
p.locks[idx].Unlock()
done()
})
}

func (p *checkerPool) GetChecker(ctx context.Context) (*checker.Checker, func()) {
p.createCheckers()
checker := p.checkers[0]
Expand Down
2 changes: 1 addition & 1 deletion internal/compiler/emitHost.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ type emitHost struct {
}

func newEmitHost(ctx context.Context, program *Program, file *ast.SourceFile) (*emitHost, func()) {
checker, done := program.GetTypeCheckerForFile(ctx, file)
checker, done := program.GetTypeCheckerForFileNonexclusive(ctx, file)
return &emitHost{
program: program,
emitResolver: checker.GetEmitResolver(),
Expand Down
23 changes: 19 additions & 4 deletions internal/compiler/program.go
Original file line number Diff line number Diff line change
Expand Up @@ -379,8 +379,14 @@ func (p *Program) GetTypeCheckers(ctx context.Context) ([]*checker.Checker, func
// method returns the checker that was tasked with checking the file. Note that it isn't possible to mix
// types obtained from different checkers, so only non-type data (such as diagnostics or string
// representations of types) should be obtained from checkers returned by this method.
func (p *Program) GetTypeCheckerForFile(ctx context.Context, file *ast.SourceFile) (*checker.Checker, func()) {
return p.checkerPool.GetCheckerForFile(ctx, file)
func (p *Program) GetTypeCheckerForFileNonexclusive(ctx context.Context, file *ast.SourceFile) (*checker.Checker, func()) {
return p.checkerPool.GetCheckerForFileNonexclusive(ctx, file)
}

// Return a checker for the given file, locked to the current thread to prevent data races from multiple threads
// accessing the same checker. The lock will be released when the `done` function is called.
func (p *Program) GetTypeCheckerForFileExclusive(ctx context.Context, file *ast.SourceFile) (*checker.Checker, func()) {
return p.checkerPool.GetCheckerForFileExclusive(ctx, file)
}

func (p *Program) GetResolvedModule(file ast.HasFileName, moduleReference string, mode core.ResolutionMode) *module.ResolvedModule {
Expand Down Expand Up @@ -1023,7 +1029,7 @@ func (p *Program) getSemanticDiagnosticsForFileNotFilter(ctx context.Context, so
var fileChecker *checker.Checker
var done func()
if sourceFile != nil {
fileChecker, done = p.checkerPool.GetCheckerForFile(ctx, sourceFile)
fileChecker, done = p.checkerPool.GetCheckerForFileNonexclusive(ctx, sourceFile)
defer done()
}
diags := slices.Clip(sourceFile.BindDiagnostics())
Expand Down Expand Up @@ -1128,7 +1134,7 @@ func (p *Program) getSuggestionDiagnosticsForFile(ctx context.Context, sourceFil
var fileChecker *checker.Checker
var done func()
if sourceFile != nil {
fileChecker, done = p.checkerPool.GetCheckerForFile(ctx, sourceFile)
fileChecker, done = p.checkerPool.GetCheckerForFileNonexclusive(ctx, sourceFile)
defer done()
}

Expand Down Expand Up @@ -1273,6 +1279,10 @@ func (p *Program) InstantiationCount() int {
return count
}

func (p *Program) Program() *Program {
return p
}

func (p *Program) GetSourceFileMetaData(path tspath.Path) ast.SourceFileMetaData {
return p.sourceFileMetaDatas[path]
}
Expand Down Expand Up @@ -1437,6 +1447,7 @@ func CombineEmitResults(results []*EmitResult) *EmitResult {

type ProgramLike interface {
Options() *core.CompilerOptions
GetSourceFile(path string) *ast.SourceFile
GetSourceFiles() []*ast.SourceFile
GetConfigFileParsingDiagnostics() []*ast.Diagnostic
GetSyntacticDiagnostics(ctx context.Context, file *ast.SourceFile) []*ast.Diagnostic
Expand All @@ -1446,7 +1457,11 @@ type ProgramLike interface {
GetGlobalDiagnostics(ctx context.Context) []*ast.Diagnostic
GetSemanticDiagnostics(ctx context.Context, file *ast.SourceFile) []*ast.Diagnostic
GetDeclarationDiagnostics(ctx context.Context, file *ast.SourceFile) []*ast.Diagnostic
GetSuggestionDiagnostics(ctx context.Context, file *ast.SourceFile) []*ast.Diagnostic
Emit(ctx context.Context, options EmitOptions) *EmitResult
CommonSourceDirectory() string
IsSourceFileDefaultLibrary(path tspath.Path) bool
Program() *Program
}

func HandleNoEmitOnError(ctx context.Context, program ProgramLike, file *ast.SourceFile) *EmitResult {
Expand Down
2 changes: 1 addition & 1 deletion internal/execute/incremental/affectedfileshandler.go
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ func (h *affectedFilesHandler) handleDtsMayChangeOfAffectedFile(dtsMayChange dts
break
}
if typeChecker == nil {
typeChecker, done = h.program.program.GetTypeCheckerForFile(h.ctx, affectedFile)
typeChecker, done = h.program.program.GetTypeCheckerForFileExclusive(h.ctx, affectedFile)
}
aliased := checker.SkipAlias(exported, typeChecker)
if aliased == exported {
Expand Down
30 changes: 30 additions & 0 deletions internal/execute/incremental/program.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,36 @@ func (p *Program) Options() *core.CompilerOptions {
return p.snapshot.options
}

// CommonSourceDirectory implements compiler.AnyProgram interface.
func (p *Program) CommonSourceDirectory() string {
p.panicIfNoProgram("CommonSourceDirectory")
return p.program.CommonSourceDirectory()
}

// Program implements compiler.AnyProgram interface.
func (p *Program) Program() *compiler.Program {
p.panicIfNoProgram("Program")
return p.program
}

// IsSourceFileDefaultLibrary implements compiler.AnyProgram interface.
func (p *Program) IsSourceFileDefaultLibrary(path tspath.Path) bool {
p.panicIfNoProgram("IsSourceFileDefaultLibrary")
return p.program.IsSourceFileDefaultLibrary(path)
}

// GetSourceFiles implements compiler.AnyProgram interface.
func (p *Program) GetSourceFiles() []*ast.SourceFile {
p.panicIfNoProgram("GetSourceFiles")
return p.program.GetSourceFiles()
}

// GetSourceFile implements compiler.AnyProgram interface.
func (p *Program) GetSourceFile(path string) *ast.SourceFile {
p.panicIfNoProgram("GetSourceFile")
return p.program.GetSourceFile(path)
}

// GetConfigFileParsingDiagnostics implements compiler.AnyProgram interface.
func (p *Program) GetConfigFileParsingDiagnostics() []*ast.Diagnostic {
p.panicIfNoProgram("GetConfigFileParsingDiagnostics")
Expand Down Expand Up @@ -172,6 +196,12 @@ func (p *Program) GetDeclarationDiagnostics(ctx context.Context, file *ast.Sourc
return nil
}

// GetSuggestionDiagnostics implements compiler.AnyProgram interface.
func (p *Program) GetSuggestionDiagnostics(ctx context.Context, file *ast.SourceFile) []*ast.Diagnostic {
p.panicIfNoProgram("GetSuggestionDiagnostics")
return p.program.GetSuggestionDiagnostics(ctx, file) // TODO: incremental suggestion diagnostics (only relevant in editor incremental builder?)
}

// GetModeForUsageLocation implements compiler.AnyProgram interface.
func (p *Program) Emit(ctx context.Context, options compiler.EmitOptions) *compiler.EmitResult {
p.panicIfNoProgram("Emit")
Expand Down
2 changes: 1 addition & 1 deletion internal/execute/incremental/programtosnapshot.go
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ func getReferencedFiles(program *compiler.Program, file *ast.SourceFile) *collec
// We need to use a set here since the code can contain the same import twice,
// but that will only be one dependency.
// To avoid invernal conversion, the key of the referencedFiles map must be of type Path
checker, done := program.GetTypeCheckerForFile(context.TODO(), file)
checker, done := program.GetTypeCheckerForFileExclusive(context.TODO(), file)
defer done()
for _, importName := range file.Imports() {
addReferencedFilesFromImportLiteral(file, &referencedFiles, checker, importName)
Expand Down
4 changes: 2 additions & 2 deletions internal/ls/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,14 @@ func (l *LanguageService) GetSymbolAtPosition(ctx context.Context, fileName stri
if node == nil {
return nil, fmt.Errorf("%w: %s:%d", ErrNoTokenAtPosition, fileName, position)
}
checker, done := program.GetTypeCheckerForFile(ctx, file)
checker, done := program.GetTypeCheckerForFileNonexclusive(ctx, file)
defer done()
return checker.GetSymbolAtLocation(node), nil
}

func (l *LanguageService) GetSymbolAtLocation(ctx context.Context, node *ast.Node) *ast.Symbol {
program := l.GetProgram()
checker, done := program.GetTypeCheckerForFile(ctx, ast.GetSourceFileOfNode(node))
checker, done := program.GetTypeCheckerForFileNonexclusive(ctx, ast.GetSourceFileOfNode(node))
defer done()
return checker.GetSymbolAtLocation(node)
}
Expand Down
6 changes: 3 additions & 3 deletions internal/ls/completions.go
Original file line number Diff line number Diff line change
Expand Up @@ -388,7 +388,7 @@ func (l *LanguageService) getCompletionsAtPosition(
)
}

checker, done := l.GetProgram().GetTypeCheckerForFile(ctx, file)
checker, done := l.GetProgram().GetTypeCheckerForFileNonexclusive(ctx, file)
defer done()
preferences := l.UserPreferences()
data := l.getCompletionData(ctx, checker, file, position, preferences)
Expand Down Expand Up @@ -1941,7 +1941,7 @@ func (l *LanguageService) getCompletionEntriesFromSymbols(
) (uniqueNames collections.Set[string], sortedEntries []*lsproto.CompletionItem) {
closestSymbolDeclaration := getClosestSymbolDeclaration(data.contextToken, data.location)
useSemicolons := lsutil.ProbablyUsesSemicolons(file)
typeChecker, done := l.GetProgram().GetTypeCheckerForFile(ctx, file)
typeChecker, done := l.GetProgram().GetTypeCheckerForFileNonexclusive(ctx, file)
defer done()
isMemberCompletion := isMemberCompletionKind(data.completionKind)
// Tracks unique names.
Expand Down Expand Up @@ -5056,7 +5056,7 @@ func (l *LanguageService) getCompletionItemDetails(
itemData *CompletionItemData,
clientOptions *lsproto.CompletionClientCapabilities,
) *lsproto.CompletionItem {
checker, done := program.GetTypeCheckerForFile(ctx, file)
checker, done := program.GetTypeCheckerForFileNonexclusive(ctx, file)
defer done()
docFormat := getCompletionDocumentationFormat(clientOptions)
contextToken, previousToken := getRelevantTokens(position, file)
Expand Down
4 changes: 2 additions & 2 deletions internal/ls/definition.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ func (l *LanguageService) ProvideDefinition(
}
originSelectionRange := l.createLspRangeFromNode(node, file)

c, done := program.GetTypeCheckerForFile(ctx, file)
c, done := program.GetTypeCheckerForFileNonexclusive(ctx, file)
defer done()

if node.Kind == ast.KindOverrideKeyword {
Expand Down Expand Up @@ -77,7 +77,7 @@ func (l *LanguageService) ProvideTypeDefinition(
}
originSelectionRange := l.createLspRangeFromNode(node, file)

c, done := program.GetTypeCheckerForFile(ctx, file)
c, done := program.GetTypeCheckerForFileNonexclusive(ctx, file)
defer done()

node = getDeclarationNameForKeyword(node)
Expand Down
2 changes: 1 addition & 1 deletion internal/ls/hover.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ func (l *LanguageService) ProvideHover(ctx context.Context, documentURI lsproto.
// Avoid giving quickInfo for the sourceFile as a whole.
return lsproto.HoverOrNull{}, nil
}
c, done := program.GetTypeCheckerForFile(ctx, file)
c, done := program.GetTypeCheckerForFileNonexclusive(ctx, file)
defer done()
rangeNode := getNodeForQuickInfo(node)
quickInfo, documentation := l.getQuickInfoAndDocumentationForSymbol(c, c.GetSymbolAtLocation(node), rangeNode, contentFormat)
Expand Down
2 changes: 1 addition & 1 deletion internal/ls/inlay_hints.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ func (l *LanguageService) ProvideInlayHint(
program, file := l.getProgramAndFile(params.TextDocument.Uri)
quotePreference := getQuotePreference(file, l.UserPreferences())

checker, done := program.GetTypeCheckerForFile(ctx, file)
checker, done := program.GetTypeCheckerForFileNonexclusive(ctx, file)
defer done()
inlayHintState := &inlayHintState{
ctx: ctx,
Expand Down
2 changes: 1 addition & 1 deletion internal/ls/signaturehelp.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ func (l *LanguageService) GetSignatureHelpItems(
clientOptions *lsproto.SignatureHelpClientCapabilities,
docFormat lsproto.MarkupKind,
) *lsproto.SignatureHelp {
typeChecker, done := program.GetTypeCheckerForFile(ctx, sourceFile)
typeChecker, done := program.GetTypeCheckerForFileNonexclusive(ctx, sourceFile)
defer done()

// Decide whether to show signature help
Expand Down
2 changes: 1 addition & 1 deletion internal/ls/string_completions.go
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ func (l *LanguageService) getStringLiteralCompletionEntries(
node *ast.StringLiteralLike,
position int,
) *stringLiteralCompletions {
typeChecker, done := l.GetProgram().GetTypeCheckerForFile(ctx, file)
typeChecker, done := l.GetProgram().GetTypeCheckerForFileNonexclusive(ctx, file)
defer done()
parent := walkUpParentheses(node.Parent)
switch parent.Kind {
Expand Down
10 changes: 9 additions & 1 deletion internal/project/checkerpool.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type CheckerPool struct {
cond *sync.Cond
createCheckersOnce sync.Once
checkers []*checker.Checker
locks []sync.Mutex
inUse map[*checker.Checker]bool
fileAssociations map[*ast.SourceFile]int
requestAssociations map[string]int
Expand All @@ -33,6 +34,7 @@ func newCheckerPool(maxCheckers int, program *compiler.Program, log func(msg str
program: program,
maxCheckers: maxCheckers,
checkers: make([]*checker.Checker, maxCheckers),
locks: make([]sync.Mutex, maxCheckers),
inUse: make(map[*checker.Checker]bool),
requestAssociations: make(map[string]int),
log: log,
Expand All @@ -42,7 +44,7 @@ func newCheckerPool(maxCheckers int, program *compiler.Program, log func(msg str
return pool
}

func (p *CheckerPool) GetCheckerForFile(ctx context.Context, file *ast.SourceFile) (*checker.Checker, func()) {
func (p *CheckerPool) GetCheckerForFileNonexclusive(ctx context.Context, file *ast.SourceFile) (*checker.Checker, func()) {
p.mu.Lock()
defer p.mu.Unlock()

Expand Down Expand Up @@ -75,6 +77,12 @@ func (p *CheckerPool) GetCheckerForFile(ctx context.Context, file *ast.SourceFil
return checker, p.createRelease(requestID, index, checker)
}

// GetCheckerForFileExclusive is the same as GetCheckerForFile but also locks a mutex associated with the checker.
// Call `done` to free the lock.
func (p *CheckerPool) GetCheckerForFileExclusive(ctx context.Context, file *ast.SourceFile) (*checker.Checker, func()) {
panic("unimplemented") // implement if used by LS
}

func (p *CheckerPool) GetChecker(ctx context.Context) (*checker.Checker, func()) {
p.mu.Lock()
defer p.mu.Unlock()
Expand Down
3 changes: 2 additions & 1 deletion internal/testrunner/compiler_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -528,7 +528,8 @@ func createHarnessTestFile(unit *testUnit, currentDirectory string) *harnessutil

func (c *compilerTest) verifyUnionOrdering(t *testing.T) {
t.Run("union ordering", func(t *testing.T) {
checkers, done := c.result.Program.GetTypeCheckers(t.Context())
p := c.result.Program.Program()
checkers, done := p.GetTypeCheckers(t.Context())
defer done()
for _, c := range checkers {
for union := range c.UnionTypes() {
Expand Down
Loading