From 36583360b01ce195a1acfa57c7a981b3e0c2831e Mon Sep 17 00:00:00 2001 From: Eric Vergnaud Date: Fri, 8 Mar 2024 18:15:24 +0100 Subject: [PATCH 01/11] add test spec --- runtime/JavaScript/spec/BitSetSpec.js | 94 ++++++++++++++++++++ runtime/JavaScript/src/antlr4/misc/BitSet.js | 2 +- 2 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 runtime/JavaScript/spec/BitSetSpec.js diff --git a/runtime/JavaScript/spec/BitSetSpec.js b/runtime/JavaScript/spec/BitSetSpec.js new file mode 100644 index 0000000000..1b9350c667 --- /dev/null +++ b/runtime/JavaScript/spec/BitSetSpec.js @@ -0,0 +1,94 @@ +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.add(67); + expect(bs.length).toEqual(1); + expect(bs.has(67)).toBeTrue(); + }) + + it("clears 1 value", () => { + const bs = new BitSet(); + bs.add(67); + bs.remove(67) + expect(bs.length).toEqual(0); + expect(bs.has(67)).toBeFalse(); + }) + + it("sets 2 consecutive values", () => { + const bs = new BitSet(); + bs.add(67); + bs.add(68); + expect(bs.length).toEqual(2); + expect(bs.has(67)).toBeTrue(); + expect(bs.has(68)).toBeTrue(); + }) + + it("sets 2 close values", () => { + const bs = new BitSet(); + bs.add(67); + bs.add(70); + expect(bs.length).toEqual(2); + expect(bs.has(67)).toBeTrue(); + expect(bs.has(70)).toBeTrue(); + }) + + it("sets 2 distant values", () => { + const bs = new BitSet(); + bs.add(67); + bs.add(241); + expect(bs.length).toEqual(2); + expect(bs.has(67)).toBeTrue(); + expect(bs.has(241)).toBeTrue(); + }) + + it("combines 2 identical sets", () => { + const bs1 = new BitSet(); + bs1.add(67); + const bs2 = new BitSet(); + bs2.add(67); + bs1.or(bs2); + expect(bs1.length).toEqual(1); + expect(bs1.has(67)).toBeTrue(); + }) + + it("combines 2 distinct sets", () => { + const bs1 = new BitSet(); + bs1.add(67); + const bs2 = new BitSet(); + bs2.add(69); + bs1.or(bs2); + expect(bs1.length).toEqual(2); + expect(bs1.has(67)).toBeTrue(); + expect(bs1.has(69)).toBeTrue(); + }) + + it("combines 2 overlapping sets", () => { + const bs1 = new BitSet(); + bs1.add(67); + bs1.add(69); + const bs2 = new BitSet(); + bs2.add(69); + bs2.add(71); + bs1.or(bs2); + expect(bs1.length).toEqual(3); + expect(bs1.has(67)).toBeTrue(); + expect(bs1.has(69)).toBeTrue(); + expect(bs1.has(71)).toBeTrue(); + }) + + it("returns values", () => { + const bs = new BitSet(); + bs.add(67); + bs.add(69); + const values = bs.values(); + expect(values).toEqual(['67', '69']); + }) +}) diff --git a/runtime/JavaScript/src/antlr4/misc/BitSet.js b/runtime/JavaScript/src/antlr4/misc/BitSet.js index 90df4e9c7e..d407e9cb46 100644 --- a/runtime/JavaScript/src/antlr4/misc/BitSet.js +++ b/runtime/JavaScript/src/antlr4/misc/BitSet.js @@ -28,7 +28,7 @@ export default class BitSet { } values() { - return Object.keys(this.data); + return Object.keys(this.data); // .map(s => parseInt(s)) } minValue() { From cb195dade1c0e0574ece5023e321e844f6fe6024 Mon Sep 17 00:00:00 2001 From: Eric Vergnaud Date: Fri, 8 Mar 2024 18:52:41 +0100 Subject: [PATCH 02/11] use array view --- runtime/JavaScript/spec/BitSetSpec.js | 2 +- runtime/JavaScript/src/antlr4/misc/BitSet.js | 93 +++++++++++++++++--- 2 files changed, 82 insertions(+), 13 deletions(-) diff --git a/runtime/JavaScript/spec/BitSetSpec.js b/runtime/JavaScript/spec/BitSetSpec.js index 1b9350c667..deeb0b9189 100644 --- a/runtime/JavaScript/spec/BitSetSpec.js +++ b/runtime/JavaScript/spec/BitSetSpec.js @@ -89,6 +89,6 @@ describe('test BitSet', () => { bs.add(67); bs.add(69); const values = bs.values(); - expect(values).toEqual(['67', '69']); + expect(values).toEqual([67, 69]); }) }) diff --git a/runtime/JavaScript/src/antlr4/misc/BitSet.js b/runtime/JavaScript/src/antlr4/misc/BitSet.js index d407e9cb46..456c70ba03 100644 --- a/runtime/JavaScript/src/antlr4/misc/BitSet.js +++ b/runtime/JavaScript/src/antlr4/misc/BitSet.js @@ -8,31 +8,74 @@ import equalArrays from "../utils/equalArrays.js"; export default class BitSet { constructor() { - this.data = []; + this.data = new Uint32Array(1); } - add(value) { - this.data[value] = true; + add(index) { + this._checkIndex(index) + this._resize(index); + this.data[index >>> 5] |= 1 << index % 32; } - or(set) { - Object.keys(set.data).map(alt => this.add(alt), this); + has(index) { + this._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]; + remove(index) { + this._checkIndex(index) + const slot = index >>> 5; + if (slot < this.data.length) { + this.data[index >>> 5] &= ~(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); // .map(s => parseInt(s)) + 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) + this._bitCount(t - 1); + l ^= t; + } + } + return result; } minValue() { - return Math.min.apply(null, this.values()); + for (let k = 0; k < 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() { @@ -48,6 +91,32 @@ export default class BitSet { } get length(){ - return this.values().length; + return this.data.map(l => this._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; + } + + _checkIndex(index) { + if(index < 0) + throw new RangeError("index cannot be negative"); + } + + _bitCount(l) { + 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; } } From e6ca29dc0a8b5b8cb50a45e64b802163ec86edff Mon Sep 17 00:00:00 2001 From: Eric Vergnaud Date: Fri, 8 Mar 2024 18:59:31 +0100 Subject: [PATCH 03/11] oops --- runtime/JavaScript/src/antlr4/misc/BitSet.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runtime/JavaScript/src/antlr4/misc/BitSet.js b/runtime/JavaScript/src/antlr4/misc/BitSet.js index 456c70ba03..bd9ae68344 100644 --- a/runtime/JavaScript/src/antlr4/misc/BitSet.js +++ b/runtime/JavaScript/src/antlr4/misc/BitSet.js @@ -64,7 +64,7 @@ export default class BitSet { } minValue() { - for (let k = 0; k < length; ++k) { + for (let k = 0; k < this.data.length; ++k) { let l = this.data[k]; if (l !== 0) { let result = 0; From 967be769a4ca8a9a02475cc2bd3be012c992b8f9 Mon Sep 17 00:00:00 2001 From: Eric Vergnaud Date: Fri, 8 Mar 2024 19:08:29 +0100 Subject: [PATCH 04/11] refactor BitSet API Signed-off-by: Eric Vergnaud --- runtime/JavaScript/spec/BitSetSpec.js | 66 +++++++++---------- .../JavaScript/src/antlr4/atn/LL1Analyzer.js | 12 ++-- .../src/antlr4/atn/ParserATNSimulator.js | 10 +-- .../src/antlr4/atn/PredictionMode.js | 4 +- .../antlr4/error/DiagnosticErrorListener.js | 2 +- runtime/JavaScript/src/antlr4/misc/BitSet.js | 6 +- 6 files changed, 50 insertions(+), 50 deletions(-) diff --git a/runtime/JavaScript/spec/BitSetSpec.js b/runtime/JavaScript/spec/BitSetSpec.js index deeb0b9189..ddbe85514c 100644 --- a/runtime/JavaScript/spec/BitSetSpec.js +++ b/runtime/JavaScript/spec/BitSetSpec.js @@ -9,85 +9,85 @@ describe('test BitSet', () => { it("sets 1 value", () => { const bs = new BitSet(); - bs.add(67); + bs.set(67); expect(bs.length).toEqual(1); - expect(bs.has(67)).toBeTrue(); + expect(bs.get(67)).toBeTrue(); }) it("clears 1 value", () => { const bs = new BitSet(); - bs.add(67); - bs.remove(67) + bs.set(67); + bs.clear(67) expect(bs.length).toEqual(0); - expect(bs.has(67)).toBeFalse(); + expect(bs.get(67)).toBeFalse(); }) it("sets 2 consecutive values", () => { const bs = new BitSet(); - bs.add(67); - bs.add(68); + bs.set(67); + bs.set(68); expect(bs.length).toEqual(2); - expect(bs.has(67)).toBeTrue(); - expect(bs.has(68)).toBeTrue(); + expect(bs.get(67)).toBeTrue(); + expect(bs.get(68)).toBeTrue(); }) it("sets 2 close values", () => { const bs = new BitSet(); - bs.add(67); - bs.add(70); + bs.set(67); + bs.set(70); expect(bs.length).toEqual(2); - expect(bs.has(67)).toBeTrue(); - expect(bs.has(70)).toBeTrue(); + expect(bs.get(67)).toBeTrue(); + expect(bs.get(70)).toBeTrue(); }) it("sets 2 distant values", () => { const bs = new BitSet(); - bs.add(67); - bs.add(241); + bs.set(67); + bs.set(241); expect(bs.length).toEqual(2); - expect(bs.has(67)).toBeTrue(); - expect(bs.has(241)).toBeTrue(); + expect(bs.get(67)).toBeTrue(); + expect(bs.get(241)).toBeTrue(); }) it("combines 2 identical sets", () => { const bs1 = new BitSet(); - bs1.add(67); + bs1.set(67); const bs2 = new BitSet(); - bs2.add(67); + bs2.set(67); bs1.or(bs2); expect(bs1.length).toEqual(1); - expect(bs1.has(67)).toBeTrue(); + expect(bs1.get(67)).toBeTrue(); }) it("combines 2 distinct sets", () => { const bs1 = new BitSet(); - bs1.add(67); + bs1.set(67); const bs2 = new BitSet(); - bs2.add(69); + bs2.set(69); bs1.or(bs2); expect(bs1.length).toEqual(2); - expect(bs1.has(67)).toBeTrue(); - expect(bs1.has(69)).toBeTrue(); + expect(bs1.get(67)).toBeTrue(); + expect(bs1.get(69)).toBeTrue(); }) it("combines 2 overlapping sets", () => { const bs1 = new BitSet(); - bs1.add(67); - bs1.add(69); + bs1.set(67); + bs1.set(69); const bs2 = new BitSet(); - bs2.add(69); - bs2.add(71); + bs2.set(69); + bs2.set(71); bs1.or(bs2); expect(bs1.length).toEqual(3); - expect(bs1.has(67)).toBeTrue(); - expect(bs1.has(69)).toBeTrue(); - expect(bs1.has(71)).toBeTrue(); + expect(bs1.get(67)).toBeTrue(); + expect(bs1.get(69)).toBeTrue(); + expect(bs1.get(71)).toBeTrue(); }) it("returns values", () => { const bs = new BitSet(); - bs.add(67); - bs.add(69); + bs.set(67); + bs.set(69); const values = bs.values(); expect(values).toEqual([67, 69]); }) 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; } - has(index) { + get(index) { this._checkIndex(index) const slot = index >>> 5; if (slot >= this.data.length) { @@ -26,7 +26,7 @@ export default class BitSet { return (this.data[slot] & 1 << index % 32) !== 0; } - remove(index) { + clear(index) { this._checkIndex(index) const slot = index >>> 5; if (slot < this.data.length) { From c3692cc91cd03097b0ec3073e50c23bba20dc315 Mon Sep 17 00:00:00 2001 From: Eric Vergnaud Date: Sat, 9 Mar 2024 12:48:50 +0100 Subject: [PATCH 05/11] document algorithm --- runtime/JavaScript/src/antlr4/misc/BitSet.js | 1 + 1 file changed, 1 insertion(+) diff --git a/runtime/JavaScript/src/antlr4/misc/BitSet.js b/runtime/JavaScript/src/antlr4/misc/BitSet.js index 2ee2e4f054..3fc2e20ebe 100644 --- a/runtime/JavaScript/src/antlr4/misc/BitSet.js +++ b/runtime/JavaScript/src/antlr4/misc/BitSet.js @@ -111,6 +111,7 @@ export default class BitSet { } _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); From 0908b58c493eae0ab7c15c9d288bfaa672e31e52 Mon Sep 17 00:00:00 2001 From: Eric Vergnaud Date: Sat, 9 Mar 2024 12:58:42 +0100 Subject: [PATCH 06/11] more tests --- runtime/JavaScript/spec/BitSetSpec.js | 16 ++++++++++++++++ runtime/JavaScript/src/antlr4/misc/BitSet.js | 14 +++++++------- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/runtime/JavaScript/spec/BitSetSpec.js b/runtime/JavaScript/spec/BitSetSpec.js index ddbe85514c..e77e8d21dd 100644 --- a/runtime/JavaScript/spec/BitSetSpec.js +++ b/runtime/JavaScript/spec/BitSetSpec.js @@ -91,4 +91,20 @@ describe('test BitSet', () => { 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/src/antlr4/misc/BitSet.js b/runtime/JavaScript/src/antlr4/misc/BitSet.js index 3fc2e20ebe..b196a94d6a 100644 --- a/runtime/JavaScript/src/antlr4/misc/BitSet.js +++ b/runtime/JavaScript/src/antlr4/misc/BitSet.js @@ -12,13 +12,13 @@ export default class BitSet { } set(index) { - this._checkIndex(index) + BitSet._checkIndex(index) this._resize(index); this.data[index >>> 5] |= 1 << index % 32; } get(index) { - this._checkIndex(index) + BitSet._checkIndex(index) const slot = index >>> 5; if (slot >= this.data.length) { return false; @@ -27,7 +27,7 @@ export default class BitSet { } clear(index) { - this._checkIndex(index) + BitSet._checkIndex(index) const slot = index >>> 5; if (slot < this.data.length) { this.data[index >>> 5] &= ~(1 << index); @@ -56,7 +56,7 @@ export default class BitSet { let l = this.data[k]; while (l !== 0) { const t = l & -l; - result[pos++] = (k << 5) + this._bitCount(t - 1); + result[pos++] = (k << 5) + BitSet._bitCount(t - 1); l ^= t; } } @@ -91,7 +91,7 @@ export default class BitSet { } get length(){ - return this.data.map(l => this._bitCount(l)).reduce((s,v) => s + v, 0); + return this.data.map(l => BitSet._bitCount(l)).reduce((s,v) => s + v, 0); } _resize(index) { @@ -105,12 +105,12 @@ export default class BitSet { this.data = data; } - _checkIndex(index) { + static _checkIndex(index) { if(index < 0) throw new RangeError("index cannot be negative"); } - _bitCount(l) { + static _bitCount(l) { // see https://graphics.stanford.edu/~seander/bithacks.html#CountBitsSetParallel let count = 0; l = l - ((l >> 1) & 0x55555555); From b49838aa375c7014b5756ee34b0bacdbd92cea81 Mon Sep 17 00:00:00 2001 From: Eric Vergnaud Date: Sun, 10 Mar 2024 13:09:21 +0100 Subject: [PATCH 07/11] fix formatting --- runtime/JavaScript/src/antlr4/misc/BitSet.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/runtime/JavaScript/src/antlr4/misc/BitSet.js b/runtime/JavaScript/src/antlr4/misc/BitSet.js index b196a94d6a..e3811c202e 100644 --- a/runtime/JavaScript/src/antlr4/misc/BitSet.js +++ b/runtime/JavaScript/src/antlr4/misc/BitSet.js @@ -64,7 +64,7 @@ export default class BitSet { } minValue() { - for (let k = 0; k < this.data.length; ++k) { + for (let k = 0; k < this.data.length; ++k) { let l = this.data[k]; if (l !== 0) { let result = 0; @@ -90,8 +90,8 @@ export default class BitSet { return "{" + this.values().join(", ") + "}"; } - get length(){ - return this.data.map(l => BitSet._bitCount(l)).reduce((s,v) => s + v, 0); + get length() { + return this.data.map(l => BitSet._bitCount(l)).reduce((s, v) => s + v, 0); } _resize(index) { @@ -106,7 +106,7 @@ export default class BitSet { } static _checkIndex(index) { - if(index < 0) + if (index < 0) throw new RangeError("index cannot be negative"); } From 2865844ecacb849694aad3429644b6c51e124c90 Mon Sep 17 00:00:00 2001 From: Eric Vergnaud Date: Sun, 10 Mar 2024 16:50:32 +0100 Subject: [PATCH 08/11] reuse slot --- runtime/JavaScript/src/antlr4/misc/BitSet.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runtime/JavaScript/src/antlr4/misc/BitSet.js b/runtime/JavaScript/src/antlr4/misc/BitSet.js index e3811c202e..f820fb2b35 100644 --- a/runtime/JavaScript/src/antlr4/misc/BitSet.js +++ b/runtime/JavaScript/src/antlr4/misc/BitSet.js @@ -30,7 +30,7 @@ export default class BitSet { BitSet._checkIndex(index) const slot = index >>> 5; if (slot < this.data.length) { - this.data[index >>> 5] &= ~(1 << index); + this.data[slot] &= ~(1 << index); } } From 7d4ad896f5bcceccc0b91e9bb1bb3943ec2ddd09 Mon Sep 17 00:00:00 2001 From: Eric Vergnaud Date: Fri, 8 Mar 2024 19:40:06 +0100 Subject: [PATCH 09/11] add test spec for HashSet Signed-off-by: Eric Vergnaud --- runtime/JavaScript/spec/HashSetSpec.js | 52 ++++++++++++++++++++++ runtime/JavaScript/spec/IntervalSetSpec.js | 5 +-- 2 files changed, 54 insertions(+), 3 deletions(-) create mode 100644 runtime/JavaScript/spec/HashSetSpec.js 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); From 1f5e64b642440f7a10c58d509efdd8fec765b639 Mon Sep 17 00:00:00 2001 From: Eric Vergnaud Date: Fri, 8 Mar 2024 20:24:41 +0100 Subject: [PATCH 10/11] improve HashSet performance Signed-off-by: Eric Vergnaud --- .../JavaScript/src/antlr4/atn/ATNConfigSet.js | 2 +- .../src/antlr4/atn/ParserATNSimulator.js | 4 +- runtime/JavaScript/src/antlr4/misc/HashSet.js | 105 +++++++++++++----- 3 files changed, 80 insertions(+), 31 deletions(-) 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/ParserATNSimulator.js b/runtime/JavaScript/src/antlr4/atn/ParserATNSimulator.js index d2153bf9f4..95b2096400 100644 --- a/runtime/JavaScript/src/antlr4/atn/ParserATNSimulator.js +++ b/runtime/JavaScript/src/antlr4/atn/ParserATNSimulator.js @@ -1287,7 +1287,7 @@ export default class ParserATNSimulator extends ATNSimulator { } c.reachesIntoOuterContext += 1; - if (closureBusy.add(c)!==c) { + if (closureBusy.getOrAdd(c)!==c) { // avoid infinite recursion for right-recursive rules continue; } @@ -1297,7 +1297,7 @@ export default class ParserATNSimulator extends ATNSimulator { console.log("dips into outer ctx: " + c); } } else { - if (!t.isEpsilon && closureBusy.add(c)!==c){ + if (!t.isEpsilon && closureBusy.getOrAdd(c)!==c){ // avoid infinite recursion for EOF* and EOF+ continue; } 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); + } + } + } } From 69cfd8e49b911c4a0483035b100fd438f1a55841 Mon Sep 17 00:00:00 2001 From: Eric Vergnaud Date: Fri, 8 Mar 2024 21:25:10 +0100 Subject: [PATCH 11/11] implement faster HashMap and test spec --- runtime/JavaScript/spec/HashMapSpec.js | 60 ++++++++++ runtime/JavaScript/src/antlr4/misc/HashMap.js | 106 +++++++++++------- .../antlr4/utils/standardEqualsFunction.js | 2 +- 3 files changed, 129 insertions(+), 39 deletions(-) create mode 100644 runtime/JavaScript/spec/HashMapSpec.js 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/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/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; }