Repository navigation
Make symbol tables insertion-ordered #64916
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
eb317b2
e1ba729
6c7193b
9f20625
5e7481b
12ad75c
d8fb3c7
1ea3e6a
77f8133
9fb3d00
3f965da
29a4a2e
bd492f0
60507f5
15d13e6
fea0ca2
27c66a6
e9c60c6
655c7fa
73d726b
70cf86f
b9981ca
4a86f9b
84b8af3
a8a6915
193faf4
7c10f46
2080da6
3374819
1423283
abcdada
8f8b066
f016c00
a8d7f4e
c141b6c
fa5d2fc
ca90e6d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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" | ||
|
|
||
|
|
@@ -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 | ||
|
jakebailey marked this conversation as resolved.
|
||
|
|
||
| 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 { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think I'd be happier if we had two funcs instead, but I suppose it's acceptable |
||
| 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) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder how many symbol tables have just 1 (or another number) items, and if we could inline the storage of the first few elements |
||
| 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 | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This prose is pretty slop, the typical "encodes the constraints of the prompt" behavior, I would maybe suggest deleting it entirely or just listing invariants