Skip to content
Open
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
10 changes: 9 additions & 1 deletion ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -357,14 +357,22 @@ func MergeTokens(newNode AstNode, oldTokens []*Token) {
// AssignContainers recursively assigns document and parent pointers for root and its subtree.
//
// It also assigns document and container on composite reference units reachable via references.
// It will also fill the [Document.References] field with all references found in the subtree.
func AssignContainers(doc *Document, root AstNode) {
references := []UntypedReference{}
doAssignContainers(doc, root, &references)
doc.References = references
}

func doAssignContainers(doc *Document, root AstNode, references *[]UntypedReference) {
root.SetDocument(doc)
root.ForEachNode(func(child AstNode, containerField unique.Handle[string], index int) {
child.SetDocument(doc)
child.SetContainer(root, containerField, index)
AssignContainers(doc, child)
doAssignContainers(doc, child, references)
})
root.ForEachReference(func(ur UntypedReference, containerField unique.Handle[string], index int) {
*references = append(*references, ur)
unit := ur.Unit()
if stringNode, ok := unit.(CompositeNode); ok {
stringNode.SetDocument(doc)
Expand Down
19 changes: 6 additions & 13 deletions linking/linker.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ package linking

import (
"context"
"sync"

core "typefox.dev/fastbelt"
"typefox.dev/fastbelt/util/parallel"
"typefox.dev/fastbelt/util/service"
)

Expand All @@ -33,17 +33,10 @@ func NewDefaultLinker(sc *service.Container) Linker {
}

func (s *DefaultLinker) Link(ctx context.Context, document *core.Document) {
waitgroup := sync.WaitGroup{}
references := []core.UntypedReference{}
root := document.Root
for node := range core.AllNodes(root) {
for ref := range core.References(node) {
references = append(references, ref)
waitgroup.Go(func() {
ref.Resolve(ctx)
})
parallel.ForEach(document.References, func(ref core.UntypedReference, _ int) {
if ctx.Err() != nil {
return
}
}
waitgroup.Wait()
document.References = references
ref.Resolve(ctx)
})
}
7 changes: 7 additions & 0 deletions util/parallel/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Copyright 2026 TypeFox GmbH
// This program and the accompanying materials are made available under the
// terms of the MIT License, which is available in the project root.

// Package parallel provides utilities for parallel iteration over slices and
// other collections.
package parallel
50 changes: 50 additions & 0 deletions util/parallel/parallel.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright 2026 TypeFox GmbH
// This program and the accompanying materials are made available under the
// terms of the MIT License, which is available in the project root.

package parallel

import (
"iter"
"runtime"
"slices"
"sync"
)

// ForEachIter calls action once for each element in seq, distributing the
// calls across at most [runtime.GOMAXPROCS](0) goroutines instead of one
// goroutine per element.
//
// The second argument to action is the zero-based index of the element. seq
// is fully consumed before any action call begins. This function blocks
// until every action call has returned.
func ForEachIter[T any](seq iter.Seq[T], action func(T, int)) {
ForEach(slices.Collect(seq), action)
}

// ForEach calls action once for each element in the given slice,
// distributing the calls across at most [runtime.GOMAXPROCS](0) goroutines
// instead of one goroutine per element.
//
// The second argument to action is the zero-based index of the element.
// This function blocks until every action call has returned.
func ForEach[T any](elements []T, action func(T, int)) {
if len(elements) == 0 {
return
}
workers := min(runtime.GOMAXPROCS(0), len(elements))
var wg sync.WaitGroup
chunk := (len(elements) + workers - 1) / workers
for i := range workers {
start, end := i*chunk, min((i+1)*chunk, len(elements))
if start >= end {
continue
}
wg.Go(func() {
for index := start; index < end; index++ {
action(elements[index], index)
}
})
}
wg.Wait()
}
39 changes: 39 additions & 0 deletions util/parallel/parallel_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Copyright 2026 TypeFox GmbH
// This program and the accompanying materials are made available under the
// terms of the MIT License, which is available in the project root.

package parallel

import (
"sync/atomic"
"testing"
)

func TestParallelForEach(t *testing.T) {
t.Run("visits every element exactly once with correct indices", func(t *testing.T) {
elements := make([]int, 500)
for i := range elements {
elements[i] = i
}
seen := make([]int32, len(elements))
ForEach(elements, func(value int, index int) {
atomic.AddInt32(&seen[index], 1)
if value != index {
t.Errorf("expected value %d at index %d, got %d", index, index, value)
}
})
for i, count := range seen {
if count != 1 {
t.Errorf("expected index %d to be visited once, got %d", i, count)
}
}
})

t.Run("empty sequence", func(t *testing.T) {
called := false
ForEach([]int{}, func(int, int) { called = true })
if called {
t.Error("expected action not to be called for empty sequence")
}
})
}
175 changes: 87 additions & 88 deletions workspace/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (

core "typefox.dev/fastbelt"
"typefox.dev/fastbelt/linking"
"typefox.dev/fastbelt/util/parallel"
"typefox.dev/fastbelt/util/service"
)

Expand Down Expand Up @@ -63,30 +64,26 @@ func (s *DefaultBuilder) Build(ctx context.Context, docs []*core.Document, downg
// PHASE 1: Parse, and compute exports (parallel per document).
parser := service.MustGet[DocumentParser](s.sc)
exporter := service.MustGet[linking.SymbolExporter](s.sc)
var phase1 sync.WaitGroup
for _, doc := range docs {
phase1.Go(func() {
if ctx.Err() != nil {
return
}
// STEP 1.1: Parse the document and create the AST.
if !doc.State.Has(core.DocStateParsed) {
parser.Parse(doc)
doc.State = doc.State.With(core.DocStateParsed)
s.notifyListeners(ctx, core.DocStateParsed, doc)
}
if ctx.Err() != nil {
return
}
// STEP 1.2: Compute the exported symbols for cross-document references.
if !doc.State.Has(core.DocStateExportedSymbols) {
exporter.ExportSymbols(ctx, doc)
doc.State = doc.State.With(core.DocStateExportedSymbols)
s.notifyListeners(ctx, core.DocStateExportedSymbols, doc)
}
})
}
phase1.Wait()
parallel.ForEach(docs, func(doc *core.Document, _ int) {
if ctx.Err() != nil {
return
}
// STEP 1.1: Parse the document and create the AST.
if !doc.State.Has(core.DocStateParsed) {
parser.Parse(doc)
doc.State = doc.State.With(core.DocStateParsed)
s.notifyListeners(ctx, core.DocStateParsed, doc)
}
if ctx.Err() != nil {
return
}
// STEP 1.2: Compute the exported symbols for cross-document references.
if !doc.State.Has(core.DocStateExportedSymbols) {
exporter.ExportSymbols(ctx, doc)
doc.State = doc.State.With(core.DocStateExportedSymbols)
s.notifyListeners(ctx, core.DocStateExportedSymbols, doc)
}
})

if err := ctx.Err(); err != nil {
return err
Expand All @@ -99,51 +96,52 @@ func (s *DefaultBuilder) Build(ctx context.Context, docs []*core.Document, downg
localSymbols := service.MustGet[linking.LocalSymbolsProvider](s.sc)
linker := service.MustGet[linking.Linker](s.sc)
referenceDescriptions := service.MustGet[linking.ReferenceDescriptionsProvider](s.sc)
var phase2 sync.WaitGroup
for _, doc := range docs {
phase2.Go(func() {
if ctx.Err() != nil {
return
}
// STEP 2.1: Collect imported symbols from all other documents.
if !doc.State.Has(core.DocStateImportedSymbols) {
allDocs := documentManager.All()
importer.ImportSymbols(ctx, doc, allDocs)
doc.State = doc.State.With(core.DocStateImportedSymbols)
s.notifyListeners(ctx, core.DocStateImportedSymbols, doc)
}
if ctx.Err() != nil {
return
}
// STEP 2.2: Compute the local symbols for intra-document references.
if !doc.State.Has(core.DocStateLocalSymbols) {
localSymbols.LocalSymbols(ctx, doc)
doc.State = doc.State.With(core.DocStateLocalSymbols)
s.notifyListeners(ctx, core.DocStateLocalSymbols, doc)
}
if ctx.Err() != nil {
return
}
// STEP 2.3: Link the document to resolve all references.
if !doc.State.Has(core.DocStateLinked) {
linker.Link(ctx, doc)
doc.State = doc.State.With(core.DocStateLinked)
s.notifyListeners(ctx, core.DocStateLinked, doc)
}
if ctx.Err() != nil {
return
}
// STEP 2.4: Provide reference descriptions for the document.
if !doc.State.Has(core.DocStateReferences) {
referenceDescriptions.ReferenceDescriptions(ctx, doc)
doc.State = doc.State.With(core.DocStateReferences)
s.notifyListeners(ctx, core.DocStateReferences, doc)
}
})
}
phase2.Wait()
parallel.ForEach(docs, func(doc *core.Document, _ int) {
if ctx.Err() != nil {
return
}
// STEP 2.1: Collect imported symbols from all other documents.
if !doc.State.Has(core.DocStateImportedSymbols) {
allDocs := documentManager.All()
importer.ImportSymbols(ctx, doc, allDocs)
doc.State = doc.State.With(core.DocStateImportedSymbols)
s.notifyListeners(ctx, core.DocStateImportedSymbols, doc)
}
if ctx.Err() != nil {
return
}
// STEP 2.2: Compute the local symbols for intra-document references.
if !doc.State.Has(core.DocStateLocalSymbols) {
localSymbols.LocalSymbols(ctx, doc)
doc.State = doc.State.With(core.DocStateLocalSymbols)
s.notifyListeners(ctx, core.DocStateLocalSymbols, doc)
}
if ctx.Err() != nil {
return
}
// STEP 2.3: Link the document to resolve all references.
if !doc.State.Has(core.DocStateLinked) {
linker.Link(ctx, doc)
doc.State = doc.State.With(core.DocStateLinked)
s.notifyListeners(ctx, core.DocStateLinked, doc)
}
if ctx.Err() != nil {
return
}
// STEP 2.4: Provide reference descriptions for the document.
if !doc.State.Has(core.DocStateReferences) {
referenceDescriptions.ReferenceDescriptions(ctx, doc)
doc.State = doc.State.With(core.DocStateReferences)
s.notifyListeners(ctx, core.DocStateReferences, doc)
}
})

if err := ctx.Err(); err != nil {
// Important note: Do not downgrade the lock here!
// If we downgrade the lock here, we would allow read access to
// the workspace while the documents are in an inconsistent state.
// In most cases, the error has been triggered by a new change,
// which will trigger a new build with a re-acquired read-lock.
return err
}

Expand All @@ -155,34 +153,41 @@ func (s *DefaultBuilder) Build(ctx context.Context, docs []*core.Document, downg

// PHASE 3: Run custom validations (parallel per document).
validator := service.MustGet[DocumentValidator](s.sc)
var phase3 sync.WaitGroup
for _, doc := range docs {
phase3.Go(func() {
parallel.ForEach(docs, func(doc *core.Document, _ int) {
if ctx.Err() != nil {
return
}
if !doc.State.Has(core.DocStateValidated) {
diagnostics := validator.Validate(ctx, doc, "on-save")
if ctx.Err() != nil {
return
}
if !doc.State.Has(core.DocStateValidated) {
diagnostics := validator.Validate(ctx, doc, "on-save")
if ctx.Err() != nil {
return
}
doc.Diagnostics = diagnostics
doc.State = doc.State.With(core.DocStateValidated)
s.notifyListeners(ctx, core.DocStateValidated, doc)
}
})
}
phase3.Wait()
doc.Diagnostics = diagnostics
doc.State = doc.State.With(core.DocStateValidated)
s.notifyListeners(ctx, core.DocStateValidated, doc)
}
})

return ctx.Err()
}

func (s *DefaultBuilder) Reset(doc *core.Document, state core.DocumentState) {
if state.Has(core.DocStateParsed) && !state.Has(core.DocStateLinked) {
// Only run the reference reset if the document is supposed to stay parsed
// but not linked. In this case, we want to reset the references, so that
// they can be re-resolved when the document is built again.
for _, ref := range doc.References {
ref.Reset()
}
// If the document is supposed to be completely reset, we can let the next
// stage simply clear the reference slice.
}
if !state.Has(core.DocStateParsed) {
doc.Root = nil
doc.Tokens = core.TokenSlice{}
doc.ParserErrors = []*core.ParserError{}
doc.LexerErrors = []*core.LexerError{}
doc.References = []core.UntypedReference{}
}
if !state.Has(core.DocStateExportedSymbols) {
doc.ExportedSymbols = nil
Expand All @@ -193,12 +198,6 @@ func (s *DefaultBuilder) Reset(doc *core.Document, state core.DocumentState) {
if !state.Has(core.DocStateLocalSymbols) {
doc.LocalSymbols = nil
}
if !state.Has(core.DocStateLinked) {
for _, ref := range doc.References {
ref.Reset()
}
doc.References = []core.UntypedReference{}
}
if !state.Has(core.DocStateReferences) {
doc.ReferenceDescriptions = nil
}
Expand Down
Loading