diff --git a/runtime/JavaScript/spec/BitSetSpec.js b/runtime/JavaScript/spec/BitSetSpec.js new file mode 100644 index 0000000000..e77e8d21dd --- /dev/null +++ b/runtime/JavaScript/spec/BitSetSpec.js @@ -0,0 +1,110 @@ +import BitSet from "../src/antlr4/misc/BitSet.js"; + +describe('test BitSet', () => { + + it("is empty", () => { + const bs = new BitSet(); + expect(bs.length).toEqual(0); + }) + + it("sets 1 value", () => { + const bs = new BitSet(); + bs.set(67); + expect(bs.length).toEqual(1); + expect(bs.get(67)).toBeTrue(); + }) + + it("clears 1 value", () => { + const bs = new BitSet(); + bs.set(67); + bs.clear(67) + expect(bs.length).toEqual(0); + expect(bs.get(67)).toBeFalse(); + }) + + it("sets 2 consecutive values", () => { + const bs = new BitSet(); + bs.set(67); + bs.set(68); + expect(bs.length).toEqual(2); + expect(bs.get(67)).toBeTrue(); + expect(bs.get(68)).toBeTrue(); + }) + + it("sets 2 close values", () => { + const bs = new BitSet(); + bs.set(67); + bs.set(70); + expect(bs.length).toEqual(2); + expect(bs.get(67)).toBeTrue(); + expect(bs.get(70)).toBeTrue(); + }) + + it("sets 2 distant values", () => { + const bs = new BitSet(); + bs.set(67); + bs.set(241); + expect(bs.length).toEqual(2); + expect(bs.get(67)).toBeTrue(); + expect(bs.get(241)).toBeTrue(); + }) + + it("combines 2 identical sets", () => { + const bs1 = new BitSet(); + bs1.set(67); + const bs2 = new BitSet(); + bs2.set(67); + bs1.or(bs2); + expect(bs1.length).toEqual(1); + expect(bs1.get(67)).toBeTrue(); + }) + + it("combines 2 distinct sets", () => { + const bs1 = new BitSet(); + bs1.set(67); + const bs2 = new BitSet(); + bs2.set(69); + bs1.or(bs2); + expect(bs1.length).toEqual(2); + expect(bs1.get(67)).toBeTrue(); + expect(bs1.get(69)).toBeTrue(); + }) + + it("combines 2 overlapping sets", () => { + const bs1 = new BitSet(); + bs1.set(67); + bs1.set(69); + const bs2 = new BitSet(); + bs2.set(69); + bs2.set(71); + bs1.or(bs2); + expect(bs1.length).toEqual(3); + expect(bs1.get(67)).toBeTrue(); + expect(bs1.get(69)).toBeTrue(); + expect(bs1.get(71)).toBeTrue(); + }) + + it("returns values", () => { + const bs = new BitSet(); + bs.set(67); + bs.set(69); + const values = bs.values(); + expect(values).toEqual([67, 69]); + }) + + it("counts bits", () => { + for(let i= 0; i <= 0xFF; i++) { + // count bits the slow but easy to understand way (Kernighan method) + let count1 = 0; + let value = i; + while(value) { + if(value & 1) + count1++; + value >>= 1; + } + // count bits the fast way + const count2 = BitSet._bitCount(i); + expect(count2).toEqual(count1); + } + }) +}) diff --git a/runtime/JavaScript/spec/HashMapSpec.js b/runtime/JavaScript/spec/HashMapSpec.js new file mode 100644 index 0000000000..0f328ad9d9 --- /dev/null +++ b/runtime/JavaScript/spec/HashMapSpec.js @@ -0,0 +1,60 @@ +import HashMap from "../src/antlr4/misc/HashMap.js"; +import HashCode from "../src/antlr4/misc/HashCode.js"; + +class Thing { + + value1 = Math.random(); + value2 = Math.random(); + + hashCode() { + return HashCode.hashStuff(this.value1); + } + + equals(other) { + return other instanceof Thing + && other.value1 === this.value1 + && other.value2 === this.value2; + } +} + +describe('test HashMap', () => { + + it("sets a thing", () => { + const t1 = new Thing(); + const map = new HashMap(); + map.set("abc", t1); + expect(map.containsKey("abc")).toBeTrue(); + expect(map.containsKey("def")).toBeFalse(); + expect(map.length).toEqual(1); + }) + + it("gets a thing", () => { + const t1 = new Thing(); + const map = new HashMap(); + map.set("abc", t1); + const t2 = map.get("abc"); + expect(t2).toEqual(t1); + }) + + it("replaces a thing", () => { + const t1 = new Thing(); + const t2 = new Thing(); + const map = new HashMap(); + map.set("abc", t1); + map.set("abc", t2); + const t3 = map.get("abc"); + expect(t3).toEqual(t2); + }) + + it("returns correct length", () => { + const t1 = new Thing(); + const t2 = new Thing(); + const map = new HashMap(); + expect(map.length).toEqual(0); + map.set("abc", t1); + expect(map.length).toEqual(1); + map.set("def", t2); + expect(map.length).toEqual(2); + }) + +}); diff --git a/runtime/JavaScript/spec/HashSetSpec.js b/runtime/JavaScript/spec/HashSetSpec.js new file mode 100644 index 0000000000..542d4fb566 --- /dev/null +++ b/runtime/JavaScript/spec/HashSetSpec.js @@ -0,0 +1,52 @@ +import HashSet from "../src/antlr4/misc/HashSet.js"; +import HashCode from "../src/antlr4/misc/HashCode.js"; + +class Thing { + + value1 = Math.random(); + value2 = Math.random(); + + hashCode() { + return HashCode.hashStuff(this.value1); + } + + equals(other) { + return other instanceof Thing + && other.value1 === this.value1 + && other.value2 === this.value2; + } +} +describe('test HashSet', () => { + + it("adds a thing", () => { + const t1 = new Thing(); + const t2 = new Thing(); + const set = new HashSet(); + set.add(t1); + expect(set.has(t1)).toBeTrue(); + expect(set.has(t2)).toBeFalse(); + expect(set.length).toEqual(1); + }) + + it("adds a thing once only", () => { + const t1 = new Thing(); + const set = new HashSet(); + set.add(t1); + set.add(t1); + expect(set.has(t1)).toBeTrue(); + expect(set.length).toEqual(1); + }) + + it("adds 2 things with same hash code", () => { + const t1 = new Thing(); + const t2 = new Thing(); + t2.value1 = t1.value1; + const set = new HashSet(); + set.add(t1); + set.add(t2); + expect(set.has(t1)).toBeTrue(); + expect(set.has(t2)).toBeTrue(); + expect(set.length).toEqual(2); + }) + +}) diff --git a/runtime/JavaScript/spec/IntervalSetSpec.js b/runtime/JavaScript/spec/IntervalSetSpec.js index b40511e543..977aa45340 100644 --- a/runtime/JavaScript/spec/IntervalSetSpec.js +++ b/runtime/JavaScript/spec/IntervalSetSpec.js @@ -1,7 +1,6 @@ -import antlr4 from "../src/antlr4/index.node.js"; -const IntervalSet = antlr4.IntervalSet; +import IntervalSet from "../src/antlr4/misc/IntervalSet.js"; -describe('IntervalSet', () => { +describe('test IntervalSet', () => { it("computes interval set length", () => { const s1 = new IntervalSet(); s1.addOne(20); diff --git a/runtime/JavaScript/src/antlr4/atn/ATNConfigSet.js b/runtime/JavaScript/src/antlr4/atn/ATNConfigSet.js index 0265d56bc5..9018ea1412 100644 --- a/runtime/JavaScript/src/antlr4/atn/ATNConfigSet.js +++ b/runtime/JavaScript/src/antlr4/atn/ATNConfigSet.js @@ -101,7 +101,7 @@ export default class ATNConfigSet { if (config.reachesIntoOuterContext > 0) { this.dipsIntoOuterContext = true; } - const existing = this.configLookup.add(config); + const existing = this.configLookup.getOrAdd(config); if (existing === config) { this.cachedHashCode = -1; this.configs.push(config); // track order here diff --git a/runtime/JavaScript/src/antlr4/atn/LL1Analyzer.js b/runtime/JavaScript/src/antlr4/atn/LL1Analyzer.js index 7e6d9ed76c..7668d1ad1c 100644 --- a/runtime/JavaScript/src/antlr4/atn/LL1Analyzer.js +++ b/runtime/JavaScript/src/antlr4/atn/LL1Analyzer.js @@ -134,9 +134,9 @@ export default class LL1Analyzer { return; } if (ctx !== PredictionContext.EMPTY) { - const removed = calledRuleStack.has(s.ruleIndex); + const removed = calledRuleStack.get(s.ruleIndex); try { - calledRuleStack.remove(s.ruleIndex); + calledRuleStack.clear(s.ruleIndex); // run thru all possible stack tops in ctx for (let i = 0; i < ctx.length; i++) { const returnState = this.atn.states[ctx.getReturnState(i)]; @@ -144,7 +144,7 @@ export default class LL1Analyzer { } }finally { if (removed) { - calledRuleStack.add(s.ruleIndex); + calledRuleStack.set(s.ruleIndex); } } return; @@ -153,15 +153,15 @@ export default class LL1Analyzer { for(let j=0; j>> 5] |= 1 << index % 32; } - or(set) { - Object.keys(set.data).map(alt => this.add(alt), this); + get(index) { + BitSet._checkIndex(index) + const slot = index >>> 5; + if (slot >= this.data.length) { + return false; + } + return (this.data[slot] & 1 << index % 32) !== 0; } - remove(value) { - delete this.data[value]; + clear(index) { + BitSet._checkIndex(index) + const slot = index >>> 5; + if (slot < this.data.length) { + this.data[slot] &= ~(1 << index); + } } - has(value) { - return this.data[value] === true; + or(set) { + const minCount = Math.min(this.data.length, set.data.length); + for (let k = 0; k < minCount; ++k) { + this.data[k] |= set.data[k]; + } + if (this.data.length < set.data.length) { + this._resize((set.data.length << 5) - 1); + const c = set.data.length; + for (let k = minCount; k < c; ++k) { + this.data[k] = set.data[k]; + } + } } values() { - return Object.keys(this.data); + const result = new Array(this.length); + let pos = 0; + const length = this.data.length; + for (let k = 0; k < length; ++k) { + let l = this.data[k]; + while (l !== 0) { + const t = l & -l; + result[pos++] = (k << 5) + BitSet._bitCount(t - 1); + l ^= t; + } + } + return result; } minValue() { - return Math.min.apply(null, this.values()); + for (let k = 0; k < this.data.length; ++k) { + let l = this.data[k]; + if (l !== 0) { + let result = 0; + while ((l & 1) === 0) { + result++; + l >>= 1; + } + return result + (32 * k); + } + } + return 0; } hashCode() { @@ -47,7 +90,34 @@ export default class BitSet { return "{" + this.values().join(", ") + "}"; } - get length(){ - return this.values().length; + get length() { + return this.data.map(l => BitSet._bitCount(l)).reduce((s, v) => s + v, 0); + } + + _resize(index) { + const count = index + 32 >>> 5; + if (count <= this.data.length) { + return; + } + const data = new Uint32Array(count); + data.set(this.data); + data.fill(0, this.data.length); + this.data = data; + } + + static _checkIndex(index) { + if (index < 0) + throw new RangeError("index cannot be negative"); + } + + static _bitCount(l) { + // see https://graphics.stanford.edu/~seander/bithacks.html#CountBitsSetParallel + let count = 0; + l = l - ((l >> 1) & 0x55555555); + l = (l & 0x33333333) + ((l >> 2) & 0x33333333); + l = (l + (l >> 4)) & 0x0f0f0f0f; + l = l + (l >> 8); + l = l + (l >> 16); + return count + l & 0x3f; } } diff --git a/runtime/JavaScript/src/antlr4/misc/HashMap.js b/runtime/JavaScript/src/antlr4/misc/HashMap.js index 13c46cada2..8740816a2e 100644 --- a/runtime/JavaScript/src/antlr4/misc/HashMap.js +++ b/runtime/JavaScript/src/antlr4/misc/HashMap.js @@ -5,80 +5,110 @@ import standardEqualsFunction from "../utils/standardEqualsFunction.js"; import standardHashCodeFunction from "../utils/standardHashCodeFunction.js"; -const HASH_KEY_PREFIX = "h-"; +const DEFAULT_LOAD_FACTOR = 0.75; +const INITIAL_CAPACITY = 16 export default class HashMap { constructor(hashFunction, equalsFunction) { - this.data = {}; + this.buckets = new Array(INITIAL_CAPACITY); + this.threshold = Math.floor(INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR); + this.itemCount = 0; this.hashFunction = hashFunction || standardHashCodeFunction; this.equalsFunction = equalsFunction || standardEqualsFunction; } set(key, value) { - const hashKey = HASH_KEY_PREFIX + this.hashFunction(key); - if (hashKey in this.data) { - const entries = this.data[hashKey]; - for (let i = 0; i < entries.length; i++) { - const entry = entries[i]; - if (this.equalsFunction(key, entry.key)) { - const oldValue = entry.value; - entry.value = value; - return oldValue; - } - } - entries.push({key:key, value:value}); + this._expand(); + const slot = this._getSlot(key); + let bucket = this.buckets[slot]; + if (!bucket) { + bucket = [[key, value]]; + this.buckets[slot] = bucket; + this.itemCount++; return value; + } + const existing = bucket.find(pair => this.equalsFunction(pair[0], key), this); + if(existing) { + const result = existing[1]; + existing[1] = value; + return result; } else { - this.data[hashKey] = [{key:key, value:value}]; + bucket.push([key, value]); + this.itemCount++; return value; } } containsKey(key) { - const hashKey = HASH_KEY_PREFIX + this.hashFunction(key); - if(hashKey in this.data) { - const entries = this.data[hashKey]; - for (let i = 0; i < entries.length; i++) { - const entry = entries[i]; - if (this.equalsFunction(key, entry.key)) - return true; - } + const bucket = this._getBucket(key); + if(!bucket) { + return false; } - return false; + const existing = bucket.find(pair => this.equalsFunction(pair[0], key), this); + return !!existing; } get(key) { - const hashKey = HASH_KEY_PREFIX + this.hashFunction(key); - if(hashKey in this.data) { - const entries = this.data[hashKey]; - for (let i = 0; i < entries.length; i++) { - const entry = entries[i]; - if (this.equalsFunction(key, entry.key)) - return entry.value; - } + const bucket = this._getBucket(key); + if(!bucket) { + return null; } - return null; + const existing = bucket.find(pair => this.equalsFunction(pair[0], key), this); + return existing ? existing[1] : null; } entries() { - return Object.keys(this.data).filter(key => key.startsWith(HASH_KEY_PREFIX)).flatMap(key => this.data[key], this); + return this.buckets.filter(b => b != null).flat(1); } getKeys() { - return this.entries().map(e => e.key); + return this.entries().map(pair => pair[0]); } getValues() { - return this.entries().map(e => e.value); + return this.entries().map(pair => pair[1]); } toString() { - const ss = this.entries().map(e => '{' + e.key + ':' + e.value + '}'); + const ss = this.entries().map(e => '{' + e[0] + ':' + e[1] + '}'); return '[' + ss.join(", ") + ']'; } get length() { - return Object.keys(this.data).filter(key => key.startsWith(HASH_KEY_PREFIX)).map(key => this.data[key].length, this).reduce((accum, item) => accum + item, 0); + return this.itemCount; + } + + _getSlot(key) { + const hash = this.hashFunction(key); + return hash & this.buckets.length - 1; + } + _getBucket(key) { + return this.buckets[this._getSlot(key)]; } + + _expand() { + if (this.itemCount <= this.threshold) { + return; + } + const old_buckets = this.buckets; + const newCapacity = this.buckets.length * 2; + this.buckets = new Array(newCapacity); + this.threshold = Math.floor(newCapacity * DEFAULT_LOAD_FACTOR); + for (const bucket of old_buckets) { + if (!bucket) { + continue; + } + for (const pair of bucket) { + const slot = this._getSlot(pair[0]); + let newBucket = this.buckets[slot]; + if (!newBucket) { + newBucket = []; + this.buckets[slot] = newBucket; + } + newBucket.push(pair); + } + } + } + } diff --git a/runtime/JavaScript/src/antlr4/misc/HashSet.js b/runtime/JavaScript/src/antlr4/misc/HashSet.js index 7fe7fc2407..11ca679650 100644 --- a/runtime/JavaScript/src/antlr4/misc/HashSet.js +++ b/runtime/JavaScript/src/antlr4/misc/HashSet.js @@ -6,52 +6,68 @@ import standardHashCodeFunction from "../utils/standardHashCodeFunction.js"; import standardEqualsFunction from "../utils/standardEqualsFunction.js"; import arrayToString from "../utils/arrayToString.js"; -const HASH_KEY_PREFIX = "h-"; +const DEFAULT_LOAD_FACTOR = 0.75; +const INITIAL_CAPACITY = 16 export default class HashSet { constructor(hashFunction, equalsFunction) { - this.data = {}; + this.buckets = new Array(INITIAL_CAPACITY); + this.threshold = Math.floor(INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR); + this.itemCount = 0; this.hashFunction = hashFunction || standardHashCodeFunction; this.equalsFunction = equalsFunction || standardEqualsFunction; } - add(value) { - const key = HASH_KEY_PREFIX + this.hashFunction(value); - if (key in this.data) { - const values = this.data[key]; - for (let i = 0; i < values.length; i++) { - if (this.equalsFunction(value, values[i])) { - return values[i]; - } - } - values.push(value); - return value; - } else { - this.data[key] = [value]; + get(value) { + if(value == null) { return value; } + const bucket = this._getBucket(value) + if (!bucket) { + return null; + } + for (const e of bucket) { + if (this.equalsFunction(e, value)) { + return e; + } + } + return null; } - has(value) { - return this.get(value) != null; + add(value) { + const existing = this.getOrAdd(value); + return existing === value; } - get(value) { - const key = HASH_KEY_PREFIX + this.hashFunction(value); - if (key in this.data) { - const values = this.data[key]; - for (let i = 0; i < values.length; i++) { - if (this.equalsFunction(value, values[i])) { - return values[i]; - } + getOrAdd(value) { + this._expand(); + const slot = this._getSlot(value); + let bucket = this.buckets[slot]; + if (!bucket) { + bucket = [value]; + this.buckets[slot] = bucket; + this.itemCount++; + return value; + } + for (const existing of bucket) { + if (this.equalsFunction(existing, value)) { + return existing; } } - return null; + bucket.push(value); + this.itemCount++; + return value; + + } + + has(value) { + return this.get(value) != null; } + values() { - return Object.keys(this.data).filter(key => key.startsWith(HASH_KEY_PREFIX)).flatMap(key => this.data[key], this); + return this.buckets.filter(b => b != null).flat(1); } toString() { @@ -59,6 +75,39 @@ export default class HashSet { } get length() { - return Object.keys(this.data).filter(key => key.startsWith(HASH_KEY_PREFIX)).map(key => this.data[key].length, this).reduce((accum, item) => accum + item, 0); + return this.itemCount; + } + + _getSlot(value) { + const hash = this.hashFunction(value); + return hash & this.buckets.length - 1; + } + _getBucket(value) { + return this.buckets[this._getSlot(value)]; + } + + _expand() { + if (this.itemCount <= this.threshold) { + return; + } + const old_buckets = this.buckets; + const newCapacity = this.buckets.length * 2; + this.buckets = new Array(newCapacity); + this.threshold = Math.floor(newCapacity * DEFAULT_LOAD_FACTOR); + for (const bucket of old_buckets) { + if (!bucket) { + continue; + } + for (const o of bucket) { + const slot = this._getSlot(o); + let newBucket = this.buckets[slot]; + if (!newBucket) { + newBucket = []; + this.buckets[slot] = newBucket; + } + newBucket.push(o); + } + } + } } diff --git a/runtime/JavaScript/src/antlr4/utils/standardEqualsFunction.js b/runtime/JavaScript/src/antlr4/utils/standardEqualsFunction.js index 3269df835e..a6efbd5a24 100644 --- a/runtime/JavaScript/src/antlr4/utils/standardEqualsFunction.js +++ b/runtime/JavaScript/src/antlr4/utils/standardEqualsFunction.js @@ -3,5 +3,5 @@ * can be found in the LICENSE.txt file in the project root. */ export default function standardEqualsFunction(a, b) { - return a ? a.equals(b) : a===b; + return a && a.equals ? a.equals(b) : a===b; }