Skip to content

Commit 5c1092c

Browse files
committed
perf(enumerate): speed up with memoization
1 parent 8e8f527 commit 5c1092c

File tree

3 files changed

+30
-19
lines changed

3 files changed

+30
-19
lines changed

src/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -300,8 +300,8 @@ class RegexBuilder {
300300
* "a", "b", "aa", "bb", "aaa", "bbb", "aaaa", "bbbb", ...
301301
* ```
302302
*/
303-
enumerate() {
304-
return RE.enumerate(this.getStdRegex())
303+
*enumerate() {
304+
yield* RE.enumerate(this.getStdRegex())
305305
}
306306

307307
/**

src/regex.ts

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -754,16 +754,27 @@ function extractConcatChain(left: StdRegex, right: StdRegex): [number, StdRegex
754754
}
755755
}
756756

757-
/**
758-
* TODO
759-
*
760-
* @public
761-
*/
762-
export function* enumerate(re: StdRegex): Generator<string> {
763-
yield* enumerateAux(re)
757+
export function enumerate(regex: StdRegex): Stream.Stream<string> {
758+
return enumerateMemoized(regex, new Map())
764759
}
765760

766-
export function enumerateAux(regex: StdRegex): Stream.Stream<string> {
761+
function enumerateMemoized(
762+
regex: StdRegex,
763+
cache: Map<number, Stream.Stream<string> | undefined>
764+
): Stream.Stream<string> {
765+
const cached = cache.get(regex.hash)
766+
if (cached !== undefined) {
767+
return cached
768+
} else {
769+
const result = enumerateMemoizedAux(regex, cache)
770+
cache.set(regex.hash, result)
771+
return result
772+
}
773+
}
774+
function enumerateMemoizedAux(
775+
regex: StdRegex,
776+
cache: Map<number, Stream.Stream<string> | undefined>
777+
): Stream.Stream<string> {
767778
switch (regex.type) {
768779
case 'epsilon':
769780
return Stream.singleton('')
@@ -772,21 +783,21 @@ export function enumerateAux(regex: StdRegex): Stream.Stream<string> {
772783
case 'concat':
773784
return Stream.diagonalize(
774785
(l,r) => l+r,
775-
enumerateAux(regex.left),
776-
enumerateAux(regex.right),
786+
enumerateMemoized(regex.left, cache),
787+
enumerateMemoized(regex.right, cache),
777788
)
778789
case 'union':
779790
return Stream.interleave(
780-
enumerateAux(regex.left),
781-
enumerateAux(regex.right),
791+
enumerateMemoized(regex.left, cache),
792+
enumerateMemoized(regex.right, cache),
782793
)
783794
case 'star':
784795
return Stream.cons(
785796
'',
786797
() => Stream.diagonalize(
787798
(l,r) => l+r,
788-
enumerateAux(regex.inner),
789-
enumerateAux(regex),
799+
enumerateMemoized(regex.inner, cache),
800+
enumerateMemoized(regex, cache),
790801
)
791802
)
792803
}

test/regex.spec.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ describe('enumerate', () => {
6060
Arb.stdRegex(),
6161
inputRegex => {
6262
const regexp = RE.toRegExp(inputRegex)
63-
const allWords = RE.enumerateAux(inputRegex)
63+
const allWords = RE.enumerate(inputRegex)
6464

6565
// long words are likely result of repitiion and are less interesting to test
6666
// and also blow up memory use:
@@ -84,7 +84,7 @@ describe('enumerate', () => {
8484

8585
// get words NOT in the output by enumerating words of the complement:
8686
const inputRegexComplement = toStdRegex_ignoreBlowUp(RE.complement(inputRegex))
87-
const allComplementWords = RE.enumerateAux(inputRegexComplement)
87+
const allComplementWords = RE.enumerate(inputRegexComplement)
8888

8989
// long words are likely result of repetition and are less interesting to test
9090
// and also blow up memory:
@@ -152,7 +152,7 @@ describe('size', () => {
152152
const predicatedSize = RE.size(stdRegex)
153153
fc.pre(predicatedSize !== undefined && predicatedSize <= 100n)
154154

155-
const allWords = [...RE.enumerateAux(stdRegex)]
155+
const allWords = [...RE.enumerate(stdRegex)]
156156
assert.equal(predicatedSize, BigInt(allWords.length))
157157
}
158158
)

0 commit comments

Comments
 (0)