Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
eb317b2
Create types in a stable order when iterating symbol tables
dimitropoulos Oct 15, 2026
e1ba729
Add tests for symbol table resolution order flakiness
Andarist Oct 16, 2026
6c7193b
Visit scope variables in a stable order when looking for variable con…
Andarist Oct 16, 2026
9f20625
Resolve alias exports of export= modules in a stable order
Andarist Oct 16, 2026
5e7481b
Check export aliases in a stable order when naming a symbol through i…
Andarist Oct 16, 2026
12ad75c
Check export * collisions in a stable order
Andarist Oct 16, 2026
d8fb3c7
Look for a non-exported member's export in a stable order
Andarist Oct 16, 2026
1ea3e6a
Sort cached symbol table aliases before resolving them
Andarist Oct 16, 2026
77f8133
Check spelling suggestion candidates in a stable order
Andarist Oct 16, 2026
9fb3d00
Create index signature sibling types in a stable order
Andarist Oct 16, 2026
3f965da
Only sort scope variables whose types are not resolved yet
Andarist Oct 16, 2026
29a4a2e
Pass symbol table keys to forEachSymbolInResolutionOrder
dimitropoulos Oct 17, 2026
bd492f0
Check export * collisions after collecting them, in a stable order
Andarist Oct 18, 2026
60507f5
Only sort unresolved aliases and let forEachSymbolInResolutionOrder s…
Andarist Oct 18, 2026
15d13e6
Rename forEachSymbolInResolutionOrder to forEachSymbolTableEntry
Andarist Oct 19, 2026
fea0ca2
Add a test for late-bound index signature siblings in declaration emit
Andarist Oct 19, 2026
27c66a6
Use forEachSymbolTableEntry in getNamedMembers
Andarist Oct 19, 2026
e9c60c6
Drop the export name tie-break in createAccessFromSymbolChain
Andarist Oct 19, 2026
655c7fa
Treat an alias as resolved only when its whole chain is
Andarist Oct 19, 2026
73d726b
Create static index signature sibling types in a stable order
Andarist Oct 19, 2026
70cf86f
Rewrap the forEachSymbolTableEntry doc comment
Andarist Oct 19, 2026
b9981ca
Make symbol tables insertion-ordered
Andarist Oct 19, 2026
4a86f9b
Rely on symbol table insertion order instead of sorting
Andarist Oct 19, 2026
84b8af3
Index large symbol tables by entry position
Andarist Oct 19, 2026
a8a6915
Do not handle nil symbols in symbol tables
Andarist Oct 19, 2026
193faf4
Delete symbol table entries in place
Andarist Oct 19, 2026
7c10f46
Document symbol table invariants
Andarist Oct 19, 2026
2080da6
Iterate symbol tables directly instead of through forEachSymbolTableE…
Andarist Oct 19, 2026
3374819
Remove the unused SymbolMap.CopyFrom
Andarist Oct 19, 2026
1423283
Size a rebuilt symbol table index for its entries
Andarist Oct 19, 2026
abcdada
Start symbol tables with room for two entries
Andarist Oct 19, 2026
8f8b066
Split declared and inherited members in place in getNamedMembers
Andarist Oct 19, 2026
f016c00
Merge remote-tracking branch 'origin/main' into deterministic-trace-t…
dimitropoulos Oct 21, 2026
a8d7f4e
Iterate symbol tables directly in GetIndexInfosOfIndexSymbol
dimitropoulos Oct 21, 2026
c141b6c
Run namespace member resolution order tests single-threaded
Andarist Oct 22, 2026
fa5d2fc
Drop the redundant oddKeys parameter from keyAt
Andarist Oct 22, 2026
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
16 changes: 8 additions & 8 deletions tsc/internal/api/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ func getSourceFileSymbolIndex(sourceFile *ast.SourceFile) map[SymbolID]*ast.Symb
addSymbol(symbol.Parent())
addSymbol(symbol.ExportSymbol())
for _, table := range []ast.SymbolTable{symbol.Members(), symbol.Exports()} {
for _, child := range table {
for _, child := range table.All() {
addSymbol(child)
}
}
Expand All @@ -81,11 +81,11 @@ func getSourceFileSymbolIndex(sourceFile *ast.SourceFile) map[SymbolID]*ast.Symb
}
addSymbol(node.Symbol())
addSymbol(node.LocalSymbol())
for _, symbol := range node.Locals() {
for _, symbol := range node.Locals().All() {
addSymbol(symbol)
}
}
for _, symbol := range file.GlobalExports {
for _, symbol := range file.GlobalExports.All() {
addSymbol(symbol)
}
for _, module := range file.PatternAmbientModules {
Expand Down Expand Up @@ -3451,20 +3451,20 @@ func (s *Session) resolveSymbolTablePropertyOfSymbol(ctx context.Context, params
defer release()

symbolTable := getter(symbol)
if len(symbolTable) == 0 {
if symbolTable.Len() == 0 {
return nil, nil
}
if len(symbolTable) == 1 {
for _, sub := range symbolTable {
if symbolTable.Len() == 1 {
for _, sub := range symbolTable.All() {
if sd == nil {
return []*SymbolResponse{newFileSymbolResponse(sub)}, nil
}
return []*SymbolResponse{sd.newSymbolResponse(sub, projectID)}, nil
}
}

symbols := make([]*ast.Symbol, 0, len(symbolTable))
for _, sub := range symbolTable {
symbols := make([]*ast.Symbol, 0, symbolTable.Len())
for _, sub := range symbolTable.All() {
symbols = append(symbols, sub)
}
if sd == nil {
Expand Down
4 changes: 2 additions & 2 deletions tsc/internal/api/session_symbolresponse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ func TestSymbolReferencesIdentifyOwnerWithoutDescriptor(t *testing.T) {
assert.Assert(t, !strings.Contains(string(encoded), "contentHash"))

// A file-owned symbol's relationships are references into the same file.
member := class.Members()["property"]
member := class.Members().Get("property")
response := newFileSymbolResponse(member)
assert.DeepEqual(t, response.Parent, reference)
}
Expand All @@ -110,6 +110,6 @@ func TestContentMappedSymbolsAreSnapshotOwned(t *testing.T) {
assert.NilError(t, err)
assert.Equal(t, resolved, class)

reference := newSymbolReference(class.Members()["property"])
reference := newSymbolReference(class.Members().Get("property"))
assert.Equal(t, reference.File, "")
}
239 changes: 238 additions & 1 deletion tsc/internal/ast/symbol.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package ast

import (
"hash/maphash"
"iter"
"maps"
"slices"
"strings"
"sync/atomic"

Expand Down Expand Up @@ -164,8 +168,241 @@ func (s *Symbol) CombinedLocalAndExportSymbolFlags() SymbolFlags {
}

// SymbolTable
//
// A SymbolTable maps names to symbols and iterates in insertion order, like Strada's Map, so that iterating a table
// is deterministic and visits symbols in the order the binder and checker added them. A nil SymbolTable is an empty
// table that can be read and iterated but not written, like a nil map.
//
// The entries are kept in insertion order in one slice. Most tables are small, so a table is searched linearly until
// it has more than symbolTableLinearMax entries, and only then gets a hash index of entry positions. An entry does not
// store its key: the key is almost always the symbol's own name, so an entry holds the symbol and the key's length,
// which rejects most non-matching entries without reading the symbol. A key with other text than its symbol's name
// is kept in oddKeys. Because of that, a symbol's name must not change after the symbol is added to a table.
//
// This is not a collections.OrderedMap[string, *Symbol]: that stores every key in its key slice and again in its
// map, and gives every table a hash map, while symbol tables are numerous, long-lived and mostly tiny.
//
// A table must not be changed with Set or Delete while it is being iterated.
type SymbolTable = *SymbolMap

type SymbolTable map[string]*Symbol
const symbolTableLinearMax = 16

type SymbolMap struct {
entries []symbolMapEntry
index []int32 // open addressing table of entry positions plus one (0 is empty), only past symbolTableLinearMax
oddKeys map[int32]string
}

var symbolTableSeed = maphash.MakeSeed()

type symbolMapEntry struct {
symbol *Symbol
keyLen int32
oddKey bool
}

func NewSymbolTable(capacity int) SymbolTable {
m := &SymbolMap{}
if capacity > 0 {
m.entries = make([]symbolMapEntry, 0, capacity)
if capacity > symbolTableLinearMax {
m.index = make([]int32, indexSizeFor(capacity))
}
}
return m
}

// SymbolTableOf returns a new table holding symbol under name.
func SymbolTableOf(name string, symbol *Symbol) SymbolTable {
m := NewSymbolTable(1)
m.Set(name, symbol)
return m
}

func (m *SymbolMap) keyAt(i int) string {
if m.entries[i].oddKey {
return m.oddKeys[int32(i)]
}
return m.entries[i].symbol.Name()
}

func (m *SymbolMap) find(key string) int {
if m == nil {
return -1
}
if m.index != nil {
mask := uint64(len(m.index) - 1)
for slot := maphash.String(symbolTableSeed, key) & mask; ; slot = (slot + 1) & mask {
p := m.index[slot]
if p == 0 {
return -1
}
if i := int(p - 1); m.entries[i].keyLen == int32(len(key)) && m.keyAt(i) == key {
return i
}
}
}
keyLen := int32(len(key))
for i := range m.entries {
if m.entries[i].keyLen == keyLen && m.keyAt(i) == key {
return i
}
}
return -1
}

func (m *SymbolMap) Len() int {
if m == nil {
return 0
}
return len(m.entries)
}

func (m *SymbolMap) Get(key string) *Symbol {
if i := m.find(key); i >= 0 {
return m.entries[i].symbol
}
return nil
}

func (m *SymbolMap) Lookup(key string) (*Symbol, bool) {
if i := m.find(key); i >= 0 {
return m.entries[i].symbol, true
}
return nil, false
}

// Set replaces the symbol of an existing key in place, keeping its position, or adds the key at the end.
func (m *SymbolMap) Set(key string, symbol *Symbol) {
if i := m.find(key); i >= 0 {
m.entries[i].symbol = symbol
if !m.entries[i].oddKey && symbol.Name() != key {
m.setOddKey(int32(i), key)
}
return
}
i := int32(len(m.entries))
if cap(m.entries) == 0 {
// Most tables hold one or two entries, so start with room for two rather than growing from one.
m.entries = make([]symbolMapEntry, 0, 2)
}
m.entries = append(m.entries, symbolMapEntry{symbol: symbol, keyLen: int32(len(key))})
if symbol.Name() != key {
m.setOddKey(i, key)
}
if m.index != nil && len(m.entries)*4 <= len(m.index)*3 {
m.insertIndex(key, i)
} else if len(m.entries) > symbolTableLinearMax {
m.buildIndex()
}
}

func (m *SymbolMap) setOddKey(i int32, key string) {
if m.oddKeys == nil {
m.oddKeys = make(map[int32]string)
}
m.entries[i].oddKey = true
m.oddKeys[i] = key
}

func indexSizeFor(n int) int {
size := 32
for size*3 < n*4 {
size *= 2
}
return size
}

func (m *SymbolMap) insertIndex(key string, i int32) {
mask := uint64(len(m.index) - 1)
slot := maphash.String(symbolTableSeed, key) & mask
for m.index[slot] != 0 {
slot = (slot + 1) & mask
}
m.index[slot] = i + 1
}

func (m *SymbolMap) buildIndex() {
m.index = make([]int32, indexSizeFor(len(m.entries)))
for i := range m.entries {
m.insertIndex(m.keyAt(i), int32(i))
}
}

func (m *SymbolMap) Delete(key string) {
d := m.find(key)
if d < 0 {
return
}
m.entries = slices.Delete(m.entries, d, d+1)
if m.oddKeys != nil {
var oddKeys map[int32]string
for i, oddKey := range m.oddKeys {
if i == int32(d) {
continue
}
if i > int32(d) {
i--
}
if oddKeys == nil {
oddKeys = make(map[int32]string, len(m.oddKeys))
}
oddKeys[i] = oddKey
}
m.oddKeys = oddKeys
}
m.index = nil
if len(m.entries) > symbolTableLinearMax {
m.buildIndex()
}
}

// All iterates over the keys and symbols of the table in insertion order.
func (m *SymbolMap) All() iter.Seq2[string, *Symbol] {
return func(yield func(string, *Symbol) bool) {
if m == nil {
return
}
entries, oddKeys := m.entries, m.oddKeys
for i := range entries {
key := entries[i].symbol.Name()
if entries[i].oddKey {
key = oddKeys[int32(i)]
}
if !yield(key, entries[i].symbol) {
return
}
}
}
}

func (m *SymbolMap) Keys() iter.Seq[string] {
return func(yield func(string) bool) {
for key := range m.All() {
if !yield(key) {
return
}
}
}
}

func (m *SymbolMap) Values() iter.Seq[*Symbol] {
return func(yield func(*Symbol) bool) {
for _, symbol := range m.All() {
if !yield(symbol) {
return
}
}
}
}

func (m *SymbolMap) Clone() SymbolTable {
if m == nil {
return nil
}
clone := &SymbolMap{entries: slices.Clone(m.entries), index: slices.Clone(m.index), oddKeys: maps.Clone(m.oddKeys)}
return clone
}

const InternalSymbolNamePrefix = "\xFE" // Invalid UTF8 sequence, will never occur as IdentifierName

Expand Down
Loading