/// module TypeScript.Collections { export var DefaultStringTableCapacity = 256; class StringTableEntry { constructor(public Text: string, public HashCode: number, public Next: StringTableEntry) { } } // A table of interned strings. Faster and better than an arbitrary hashtable for the needs of the // scanner. Specifically, the scanner operates over a sliding window of characters, with a start // and end pointer for the current lexeme. The scanner then wants to get the *interned* string // represented by that subsection. // // Importantly, if the string is already interned, then it wants ask "is the string represented by // this section of a char array contained within the table" in a non-allocating fashion. i.e. if // you have "[' ', 'p', 'u', 'b', 'l', 'i', 'c', ' ']" and you ask to get the string represented by // range [1, 7), then this table will return "public" without any allocations if that value was // already in the table. // // Of course, if the value is not in the table then there will be an initial cost to allocate the // string and the bucket for the table. However, that is only incurred the first time each unique // string is added. export class StringTable { // TODO: uncomment this once typecheck bug is fixed. private entries: StringTableEntry[]; private count: number = 0; constructor(capacity: number) { var size = Hash.getPrime(capacity); this.entries = ArrayUtilities.createArray(size, null); } public addCharArray(key: number[], start: number, len: number): string { // Compute the hash for this key. Also ensure that it fits within 31 bits (so that it // stays a non-heap integer, and so we can index into the array safely). var hashCode = Hash.computeSimple31BitCharArrayHashCode(key, start, len) & 0x7FFFFFFF; // Debug.assert(hashCode > 0); // First see if we already have the string represented by "key[start, start + len)" already // present in this table. If we do, just return that string. Do this without any // allocations var entry = this.findCharArrayEntry(key, start, len, hashCode); if (entry !== null) { return entry.Text; } // We don't have an entry for that string in our table. Convert that var slice: number[] = key.slice(start, start + len); return this.addEntry(StringUtilities.fromCharCodeArray(slice), hashCode); } private findCharArrayEntry(key: number[], start: number, len: number, hashCode: number) { for (var e = this.entries[hashCode % this.entries.length]; e !== null; e = e.Next) { if (e.HashCode === hashCode && StringTable.textCharArrayEquals(e.Text, key, start, len)) { return e; } } return null; } private addEntry(text: string, hashCode: number): string { var index = hashCode % this.entries.length; var e = new StringTableEntry(text, hashCode, this.entries[index]); this.entries[index] = e; // We grow when our load factor equals 1. I tried different load factors (like .75 and // .5), however they seemed to have no effect on running time. With a load factor of 1 // we seem to get about 80% slot fill rate with an average of around 1.25 table entries // per slot. if (this.count === this.entries.length) { this.grow(); } this.count++; return e.Text; } //private dumpStats() { // var standardOut = Environment.standardOut; // standardOut.WriteLine("----------------------") // standardOut.WriteLine("String table stats"); // standardOut.WriteLine("Count : " + this.count); // standardOut.WriteLine("Entries Length : " + this.entries.length); // var longestSlot = 0; // var occupiedSlots = 0; // for (var i = 0; i < this.entries.length; i++) { // if (this.entries[i] !== null) { // occupiedSlots++; // var current = this.entries[i]; // var slotCount = 0; // while (current !== null) { // slotCount++; // current = current.Next; // } // longestSlot = MathPrototype.max(longestSlot, slotCount); // } // } // standardOut.WriteLine("Occupied slots : " + occupiedSlots); // standardOut.WriteLine("Longest slot : " + longestSlot); // standardOut.WriteLine("Avg Length/Slot : " + (this.count / occupiedSlots)); // standardOut.WriteLine("----------------------"); //} private grow(): void { // this.dumpStats(); var newSize = Hash.expandPrime(this.entries.length); var oldEntries = this.entries; var newEntries: StringTableEntry[] = ArrayUtilities.createArray(newSize, null); this.entries = newEntries; for (var i = 0; i < oldEntries.length; i++) { var e = oldEntries[i]; while (e !== null) { var newIndex = e.HashCode % newSize; var tmp = e.Next; e.Next = newEntries[newIndex]; newEntries[newIndex] = e; e = tmp; } } // this.dumpStats(); } private static textCharArrayEquals(text: string, array: number[], start: number, length: number): boolean { if (text.length !== length) { return false; } var s = start; for (var i = 0; i < length; i++) { if (text.charCodeAt(i) !== array[s]) { return false; } s++; } return true; } } export var DefaultStringTable = new StringTable(DefaultStringTableCapacity); }