@dallaylaen/ski-interpreter 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/bin/ski.js +96 -97
- package/index.js +14 -6
- package/lib/expr.js +168 -67
- package/lib/extras.js +140 -0
- package/lib/internal.js +105 -0
- package/lib/parser.js +20 -4
- package/lib/quest.js +73 -43
- package/package.json +2 -2
- package/types/index.d.ts +2 -2
- package/types/lib/expr.d.ts +111 -38
- package/types/lib/extras.d.ts +57 -0
- package/types/lib/internal.d.ts +52 -0
- package/types/lib/parser.d.ts +5 -0
- package/types/lib/quest.d.ts +56 -39
- package/lib/util.js +0 -57
- package/types/lib/util.d.ts +0 -11
package/lib/expr.js
CHANGED
|
@@ -1,17 +1,40 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { unwrap, prepareWrapper } = require('./internal');
|
|
4
|
+
|
|
3
5
|
const DEFAULTS = {
|
|
4
6
|
max: 1000,
|
|
5
7
|
maxArgs: 32,
|
|
6
8
|
};
|
|
7
9
|
|
|
10
|
+
/**
|
|
11
|
+
* @template T
|
|
12
|
+
* @typedef {T | { value: T?, action: string } | null} ActionWrapper
|
|
13
|
+
*/
|
|
14
|
+
const control = {
|
|
15
|
+
descend: prepareWrapper('descend'),
|
|
16
|
+
prune: prepareWrapper('prune'),
|
|
17
|
+
stop: prepareWrapper('stop'),
|
|
18
|
+
};
|
|
19
|
+
|
|
8
20
|
/**
|
|
9
21
|
* @typedef {Expr | function(Expr): Partial} Partial
|
|
10
22
|
*/
|
|
11
23
|
|
|
12
24
|
class Expr {
|
|
13
25
|
/**
|
|
14
|
-
* @descr A
|
|
26
|
+
* @descr A combinatory logic expression.
|
|
27
|
+
*
|
|
28
|
+
* Applications, variables, and other terms like combinators per se
|
|
29
|
+
* are subclasses of this class.
|
|
30
|
+
*
|
|
31
|
+
* @abstract
|
|
32
|
+
* @property {{
|
|
33
|
+
* scope?: any,
|
|
34
|
+
* env?: { [key: string]: Expr },
|
|
35
|
+
* src?: string,
|
|
36
|
+
* parser: object,
|
|
37
|
+
* }} [context] // TODO proper type
|
|
15
38
|
*/
|
|
16
39
|
constructor () {
|
|
17
40
|
if (new.target === Expr)
|
|
@@ -25,7 +48,10 @@ class Expr {
|
|
|
25
48
|
* @return {Expr}
|
|
26
49
|
*/
|
|
27
50
|
apply (...args) {
|
|
28
|
-
|
|
51
|
+
let expr = this;
|
|
52
|
+
for (const arg of args)
|
|
53
|
+
expr = new App(expr, arg);
|
|
54
|
+
return expr;
|
|
29
55
|
}
|
|
30
56
|
|
|
31
57
|
/**
|
|
@@ -66,7 +92,36 @@ class Expr {
|
|
|
66
92
|
}
|
|
67
93
|
|
|
68
94
|
/**
|
|
69
|
-
* @desc
|
|
95
|
+
* @desc Fold the expression into a single value by recursively applying combine() to its subterms.
|
|
96
|
+
* Nodes are traversed in leftmost-outermost order, i.e. the same order as reduction steps are taken.
|
|
97
|
+
*
|
|
98
|
+
* null or undefined return value from combine() means "keep current value and descend further".
|
|
99
|
+
*
|
|
100
|
+
* SKI.control provides primitives to control the folding flow:
|
|
101
|
+
* - SKI.control.prune(value) means "use value and don't descend further into this branch";
|
|
102
|
+
* - SKI.control.stop(value) means "stop folding immediately and return value".
|
|
103
|
+
* - SKI.control.descend(value) is the default behavior, meaning "use value and descend further".
|
|
104
|
+
*
|
|
105
|
+
* This method is experimental and may change in the future.
|
|
106
|
+
*
|
|
107
|
+
* @experimental
|
|
108
|
+
* @template T
|
|
109
|
+
* @param {T} initial
|
|
110
|
+
* @param {(acc: T, expr: Expr) => ActionWrapper<T>} combine
|
|
111
|
+
* @returns {T}
|
|
112
|
+
*/
|
|
113
|
+
|
|
114
|
+
fold (initial, combine) {
|
|
115
|
+
const [value, _] = unwrap(this._fold(initial, combine));
|
|
116
|
+
return value ?? initial;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
_fold (initial, combine) {
|
|
120
|
+
return combine(initial, this);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* @desc rough estimate of the complexity of the term
|
|
70
125
|
* @return {number}
|
|
71
126
|
*/
|
|
72
127
|
weight () {
|
|
@@ -85,7 +140,7 @@ class Expr {
|
|
|
85
140
|
*
|
|
86
141
|
* Use toLambda() if you want to get a lambda term in any case.
|
|
87
142
|
*
|
|
88
|
-
* @param {{max
|
|
143
|
+
* @param {{max?: number, maxArgs?: number}} options
|
|
89
144
|
* @return {{
|
|
90
145
|
* normal: boolean,
|
|
91
146
|
* steps: number,
|
|
@@ -105,6 +160,14 @@ class Expr {
|
|
|
105
160
|
return out;
|
|
106
161
|
}
|
|
107
162
|
|
|
163
|
+
/**
|
|
164
|
+
*
|
|
165
|
+
* @param {{max: number, maxArgs: number, index: number}} options
|
|
166
|
+
* @param {FreeVar[]} preArgs
|
|
167
|
+
* @param {number} steps
|
|
168
|
+
* @returns {{normal: boolean, steps: number}|{normal: boolean, steps: number}|{normal: boolean, steps: number, expr: Lambda|*, arity?: *, skip?: Set<any>, dup?: Set<any>, duplicate, discard, proper: boolean}|*|{normal: boolean, steps: number}}
|
|
169
|
+
* @private
|
|
170
|
+
*/
|
|
108
171
|
_infer (options, preArgs = [], steps = 0) {
|
|
109
172
|
if (preArgs.length > options.maxArgs || steps > options.max)
|
|
110
173
|
return { normal: false, steps };
|
|
@@ -130,7 +193,7 @@ class Expr {
|
|
|
130
193
|
|
|
131
194
|
// adding more args won't help, bail out
|
|
132
195
|
// if we're an App, the App's _infer will take care of further args
|
|
133
|
-
if (this.
|
|
196
|
+
if (this.unroll()[0] instanceof FreeVar)
|
|
134
197
|
return { normal: false, steps };
|
|
135
198
|
|
|
136
199
|
// try adding more arguments, maybe we'll get a normal form then
|
|
@@ -138,18 +201,23 @@ class Expr {
|
|
|
138
201
|
return this.apply(push)._infer(options, [...preArgs, push], steps);
|
|
139
202
|
}
|
|
140
203
|
|
|
141
|
-
|
|
204
|
+
/**
|
|
205
|
+
* @desc Expand an expression into a list of terms
|
|
206
|
+
* that give the initial expression when applied from left to right:
|
|
207
|
+
* ((a, b), (c, d)) => [a, b, (c, d)]
|
|
208
|
+
*
|
|
209
|
+
* This can be thought of as an opposite of apply:
|
|
210
|
+
* fun.apply(...arg).unroll() is exactly [fun, ...args]
|
|
211
|
+
* (even if ...arg is in fact empty).
|
|
212
|
+
*
|
|
213
|
+
* @returns {Expr[]}
|
|
214
|
+
*/
|
|
215
|
+
unroll () {
|
|
142
216
|
// currently only used by infer() but may be useful
|
|
143
217
|
// to convert binary App trees to n-ary or smth
|
|
144
218
|
return [this];
|
|
145
219
|
}
|
|
146
220
|
|
|
147
|
-
_firstVar () {
|
|
148
|
-
// boolean, whether the expression starts with a free variable
|
|
149
|
-
// only used by infer() as a shortcut to this._aslist()[0] instanceof FreeVar
|
|
150
|
-
return false;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
221
|
/**
|
|
154
222
|
* @desc Returns a series of lambda terms equivalent to the given expression,
|
|
155
223
|
* up to the provided computation steps limit,
|
|
@@ -172,7 +240,14 @@ class Expr {
|
|
|
172
240
|
* @return {IterableIterator<{expr: Expr, steps: number?, comment: string?}>}
|
|
173
241
|
*/
|
|
174
242
|
* toLambda (options = {}) {
|
|
175
|
-
const expr =
|
|
243
|
+
const expr = this.traverse(e => {
|
|
244
|
+
if (e instanceof FreeVar || e instanceof App || e instanceof Lambda || e instanceof Alias)
|
|
245
|
+
return null; // no change
|
|
246
|
+
const guess = e.infer({ max: options.max, maxArgs: options.maxArgs });
|
|
247
|
+
if (!guess.normal)
|
|
248
|
+
throw new Error('Failed to infer an equivalent lambda term for ' + e);
|
|
249
|
+
return guess.expr;
|
|
250
|
+
}) ?? this;
|
|
176
251
|
yield * simplifyLambda(expr, options);
|
|
177
252
|
}
|
|
178
253
|
|
|
@@ -183,7 +258,7 @@ class Expr {
|
|
|
183
258
|
*
|
|
184
259
|
* See also Expr.walk() and Expr.toLambda().
|
|
185
260
|
*
|
|
186
|
-
* @param {{max
|
|
261
|
+
* @param {{max?: number}} [options]
|
|
187
262
|
* @return {IterableIterator<{final: boolean, expr: Expr, steps: number}>}
|
|
188
263
|
*/
|
|
189
264
|
* toSKI (options = {}) {
|
|
@@ -202,6 +277,12 @@ class Expr {
|
|
|
202
277
|
}
|
|
203
278
|
}
|
|
204
279
|
|
|
280
|
+
/**
|
|
281
|
+
* @desc Internal method for toSKI, which performs one step of the conversion.
|
|
282
|
+
* @param {{max: number, steps: number}} options
|
|
283
|
+
* @returns {Expr}
|
|
284
|
+
* @private
|
|
285
|
+
*/
|
|
205
286
|
_rski (options) {
|
|
206
287
|
return this;
|
|
207
288
|
}
|
|
@@ -394,6 +475,12 @@ class Expr {
|
|
|
394
475
|
return false;
|
|
395
476
|
}
|
|
396
477
|
|
|
478
|
+
/**
|
|
479
|
+
* @desc Whether the expression can be printed without a space when followed by arg.
|
|
480
|
+
* @param {Expr} arg
|
|
481
|
+
* @returns {boolean}
|
|
482
|
+
* @private
|
|
483
|
+
*/
|
|
397
484
|
_unspaced (arg) {
|
|
398
485
|
return this._braced(true);
|
|
399
486
|
}
|
|
@@ -431,7 +518,6 @@ class Expr {
|
|
|
431
518
|
* @example foo.format({ inventory: { T } }) // use T as a named term, expand all others
|
|
432
519
|
*
|
|
433
520
|
*/
|
|
434
|
-
|
|
435
521
|
format (options = {}) {
|
|
436
522
|
const fallback = options.html
|
|
437
523
|
? {
|
|
@@ -463,28 +549,37 @@ class Expr {
|
|
|
463
549
|
}, 0);
|
|
464
550
|
}
|
|
465
551
|
|
|
552
|
+
/**
|
|
553
|
+
* @desc Internal method for format(), which performs the actual formatting.
|
|
554
|
+
* @param {Object} options
|
|
555
|
+
* @param {number} nargs
|
|
556
|
+
* @returns {string}
|
|
557
|
+
* @private
|
|
558
|
+
*/
|
|
466
559
|
_format (options, nargs) {
|
|
467
560
|
throw new Error( 'No _format() method defined in class ' + this.constructor.name );
|
|
468
561
|
}
|
|
469
562
|
|
|
470
563
|
// output: string[] /* appended */, inventory: { [key: string]: Expr }, seen: Set<Expr>
|
|
471
564
|
_declare (output, inventory, seen) {}
|
|
565
|
+
|
|
566
|
+
toJSON () {
|
|
567
|
+
return this.format();
|
|
568
|
+
}
|
|
472
569
|
}
|
|
473
570
|
|
|
474
571
|
class App extends Expr {
|
|
475
572
|
/**
|
|
476
573
|
* @desc Application of fun() to args.
|
|
477
|
-
* Never ever use new App(fun,
|
|
574
|
+
* Never ever use new App(fun, arg) directly, use fun.apply(...args) instead.
|
|
478
575
|
* @param {Expr} fun
|
|
479
|
-
* @param {Expr}
|
|
576
|
+
* @param {Expr} arg
|
|
480
577
|
*/
|
|
481
|
-
constructor (fun,
|
|
482
|
-
if (args.length === 0)
|
|
483
|
-
throw new Error('Attempt to create an application with no arguments (likely interpreter bug)');
|
|
578
|
+
constructor (fun, arg) {
|
|
484
579
|
super();
|
|
485
580
|
|
|
486
|
-
this.arg =
|
|
487
|
-
this.fun =
|
|
581
|
+
this.arg = arg;
|
|
582
|
+
this.fun = fun;
|
|
488
583
|
this.final = false;
|
|
489
584
|
this.arity = this.fun.arity > 0 ? this.fun.arity - 1 : 0;
|
|
490
585
|
}
|
|
@@ -509,7 +604,7 @@ class App extends Expr {
|
|
|
509
604
|
return proxy;
|
|
510
605
|
steps = proxy.steps; // reimport extra iterations
|
|
511
606
|
|
|
512
|
-
const [first, ...list] = this.
|
|
607
|
+
const [first, ...list] = this.unroll();
|
|
513
608
|
if (!(first instanceof FreeVar))
|
|
514
609
|
return { normal: false, steps }
|
|
515
610
|
// TODO maybe do it later
|
|
@@ -535,17 +630,13 @@ class App extends Expr {
|
|
|
535
630
|
return {
|
|
536
631
|
normal: true,
|
|
537
632
|
steps,
|
|
538
|
-
...maybeLambda(preArgs,
|
|
633
|
+
...maybeLambda(preArgs, first.apply(...out), {
|
|
539
634
|
discard,
|
|
540
635
|
duplicate,
|
|
541
636
|
}),
|
|
542
637
|
};
|
|
543
638
|
}
|
|
544
639
|
|
|
545
|
-
_firstVar () {
|
|
546
|
-
return this.fun._firstVar();
|
|
547
|
-
}
|
|
548
|
-
|
|
549
640
|
expand () {
|
|
550
641
|
return this.fun.expand().apply(this.arg.expand());
|
|
551
642
|
}
|
|
@@ -568,6 +659,21 @@ class App extends Expr {
|
|
|
568
659
|
return predicate(this) || this.fun.any(predicate) || this.arg.any(predicate);
|
|
569
660
|
}
|
|
570
661
|
|
|
662
|
+
_fold (initial, combine) {
|
|
663
|
+
const [value = initial, action = 'descend'] = unwrap(combine(initial, this));
|
|
664
|
+
if (action === 'prune')
|
|
665
|
+
return value;
|
|
666
|
+
if (action === 'stop')
|
|
667
|
+
return control.stop(value);
|
|
668
|
+
const [fValue = value, fAction = 'descend'] = unwrap(this.fun._fold(value, combine));
|
|
669
|
+
if (fAction === 'stop')
|
|
670
|
+
return control.stop(fValue);
|
|
671
|
+
const [aValue = fValue, aAction = 'descend'] = unwrap(this.arg._fold(fValue, combine));
|
|
672
|
+
if (aAction === 'stop')
|
|
673
|
+
return control.stop(aValue);
|
|
674
|
+
return aValue;
|
|
675
|
+
}
|
|
676
|
+
|
|
571
677
|
subst (search, replace) {
|
|
572
678
|
const fun = this.fun.subst(search, replace);
|
|
573
679
|
const arg = this.arg.subst(search, replace);
|
|
@@ -622,13 +728,8 @@ class App extends Expr {
|
|
|
622
728
|
}
|
|
623
729
|
}
|
|
624
730
|
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
return [this.fun, this.arg];
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
_aslist () {
|
|
631
|
-
return [...this.fun._aslist(), this.arg];
|
|
731
|
+
unroll () {
|
|
732
|
+
return [...this.fun.unroll(), this.arg];
|
|
632
733
|
}
|
|
633
734
|
|
|
634
735
|
/**
|
|
@@ -740,10 +841,6 @@ class FreeVar extends Named {
|
|
|
740
841
|
return 0;
|
|
741
842
|
}
|
|
742
843
|
|
|
743
|
-
_firstVar () {
|
|
744
|
-
return true;
|
|
745
|
-
}
|
|
746
|
-
|
|
747
844
|
diff (other, swap = false) {
|
|
748
845
|
if (!(other instanceof FreeVar))
|
|
749
846
|
return super.diff(other, swap);
|
|
@@ -899,6 +996,18 @@ class Lambda extends Expr {
|
|
|
899
996
|
return predicate(this) || this.impl.any(predicate);
|
|
900
997
|
}
|
|
901
998
|
|
|
999
|
+
_fold (initial, combine) {
|
|
1000
|
+
const [value = initial, action = 'descend'] = unwrap(combine(initial, this));
|
|
1001
|
+
if (action === 'prune')
|
|
1002
|
+
return value;
|
|
1003
|
+
if (action === 'stop')
|
|
1004
|
+
return control.stop(value);
|
|
1005
|
+
const [iValue, iAction] = unwrap(this.impl._fold(value, combine));
|
|
1006
|
+
if (iAction === 'stop')
|
|
1007
|
+
return control.stop(iValue);
|
|
1008
|
+
return iValue ?? value;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
902
1011
|
subst (search, replace) {
|
|
903
1012
|
if (search === this.arg)
|
|
904
1013
|
return null;
|
|
@@ -920,14 +1029,14 @@ class Lambda extends Expr {
|
|
|
920
1029
|
if (!impl.any(e => e === this.arg))
|
|
921
1030
|
return native.K.apply(impl);
|
|
922
1031
|
if (impl instanceof App) {
|
|
923
|
-
const
|
|
1032
|
+
const { fun, arg } = impl;
|
|
924
1033
|
// try eta reduction
|
|
925
|
-
if (
|
|
926
|
-
return
|
|
1034
|
+
if (arg === this.arg && !fun.any(e => e === this.arg))
|
|
1035
|
+
return fun._rski(options);
|
|
927
1036
|
// fall back to S
|
|
928
1037
|
return native.S.apply(
|
|
929
|
-
(new Lambda(this.arg,
|
|
930
|
-
(new Lambda(this.arg,
|
|
1038
|
+
(new Lambda(this.arg, fun))._rski(options),
|
|
1039
|
+
(new Lambda(this.arg, arg))._rski(options)
|
|
931
1040
|
);
|
|
932
1041
|
}
|
|
933
1042
|
throw new Error('Don\'t know how to convert to SKI' + this);
|
|
@@ -1056,6 +1165,18 @@ class Alias extends Named {
|
|
|
1056
1165
|
return predicate(this) || this.impl.any(predicate);
|
|
1057
1166
|
}
|
|
1058
1167
|
|
|
1168
|
+
_fold (initial, combine) {
|
|
1169
|
+
const [value = initial, action = 'descend'] = unwrap(combine(initial, this));
|
|
1170
|
+
if (action === 'prune')
|
|
1171
|
+
return value;
|
|
1172
|
+
if (action === 'stop')
|
|
1173
|
+
return control.stop(value);
|
|
1174
|
+
const [iValue, iAction] = unwrap(this.impl._fold(value, combine));
|
|
1175
|
+
if (iAction === 'stop')
|
|
1176
|
+
return control.stop(iValue);
|
|
1177
|
+
return iValue ?? value;
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1059
1180
|
subst (search, replace) {
|
|
1060
1181
|
if (this === search)
|
|
1061
1182
|
return replace;
|
|
@@ -1078,10 +1199,6 @@ class Alias extends Named {
|
|
|
1078
1199
|
return { expr: this.impl, steps: 0, changed: true };
|
|
1079
1200
|
}
|
|
1080
1201
|
|
|
1081
|
-
_firstVar () {
|
|
1082
|
-
return this.impl._firstVar();
|
|
1083
|
-
}
|
|
1084
|
-
|
|
1085
1202
|
diff (other, swap = false) {
|
|
1086
1203
|
if (this === other)
|
|
1087
1204
|
return null;
|
|
@@ -1230,23 +1347,6 @@ function maybeLambda (args, expr, caps = {}) {
|
|
|
1230
1347
|
};
|
|
1231
1348
|
}
|
|
1232
1349
|
|
|
1233
|
-
function naiveCanonize (expr) {
|
|
1234
|
-
if (expr instanceof App)
|
|
1235
|
-
return naiveCanonize(expr.fun).apply(naiveCanonize(expr.arg));
|
|
1236
|
-
|
|
1237
|
-
if (expr instanceof Lambda)
|
|
1238
|
-
return new Lambda(expr.arg, naiveCanonize(expr.impl));
|
|
1239
|
-
|
|
1240
|
-
if (expr instanceof Alias)
|
|
1241
|
-
return naiveCanonize(expr.impl);
|
|
1242
|
-
|
|
1243
|
-
const canon = expr.infer();
|
|
1244
|
-
if (canon.expr)
|
|
1245
|
-
return canon.expr;
|
|
1246
|
-
|
|
1247
|
-
throw new Error('Failed to canonize expression: ' + expr);
|
|
1248
|
-
}
|
|
1249
|
-
|
|
1250
1350
|
function nthvar (n) {
|
|
1251
1351
|
return new FreeVar('abcdefgh'[n] ?? 'x' + n);
|
|
1252
1352
|
}
|
|
@@ -1284,7 +1384,7 @@ function * simplifyLambda (expr, options = {}, state = { steps: 0 }) {
|
|
|
1284
1384
|
// fun * arg Descartes product
|
|
1285
1385
|
if (expr instanceof App) {
|
|
1286
1386
|
// try to split into fun+arg, then try canonization but exposing each step
|
|
1287
|
-
let
|
|
1387
|
+
let { fun, arg } = expr;
|
|
1288
1388
|
|
|
1289
1389
|
for (const term of simplifyLambda(fun, options, state)) {
|
|
1290
1390
|
const candidate = term.expr.apply(arg);
|
|
@@ -1312,5 +1412,6 @@ function * simplifyLambda (expr, options = {}, state = { steps: 0 }) {
|
|
|
1312
1412
|
|
|
1313
1413
|
Expr.declare = declare;
|
|
1314
1414
|
Expr.native = native;
|
|
1415
|
+
Expr.control = control;
|
|
1315
1416
|
|
|
1316
|
-
module.exports = { Expr, App, FreeVar, Lambda, Native, Alias, Church };
|
|
1417
|
+
module.exports = { Expr, App, Named, FreeVar, Lambda, Native, Alias, Church };
|
package/lib/extras.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { Expr } = require('./expr')
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @desc Extra utilities that do not belong in the core.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @experimental
|
|
11
|
+
* @desc Look for an expression that matches the predicate,
|
|
12
|
+
* starting with the seed and applying the terms to one another.
|
|
13
|
+
*
|
|
14
|
+
* A predicate returning 0 (or nothing) means "keep looking",
|
|
15
|
+
* a positive number stands for "found",
|
|
16
|
+
* and a negative means "discard this term from further applications".
|
|
17
|
+
*
|
|
18
|
+
* The order of search is from shortest to longest expressions.
|
|
19
|
+
*
|
|
20
|
+
* @param {Expr[]} seed
|
|
21
|
+
* @param {object} options
|
|
22
|
+
* @param {number} [options.depth] - maximum generation to search for
|
|
23
|
+
* @param {number} [options.tries] - maximum number of tries before giving up
|
|
24
|
+
* @param {boolean} [options.infer] - whether to call infer(), default true.
|
|
25
|
+
* @param {number} [options.maxArgs] - arguments in infer()
|
|
26
|
+
* @param {number} [options.max] - step limit in infer()
|
|
27
|
+
* @param {boolean} [options.noskip] - prevents skipping equivalent terms. Always true if infer is false.
|
|
28
|
+
* @param {boolean} [retain] - if true. also add the whole cache to returned value
|
|
29
|
+
* @param {({gen: number, total: number, probed: number, step: boolean}) => void} [options.progress]
|
|
30
|
+
* @param {number} [options.progressInterval] - minimum number of tries between calls to options.progress, default 1000.
|
|
31
|
+
* @param {(e: Expr, props: {}) => number?} predicate
|
|
32
|
+
* @return {{expr?: Expr, total: number, probed: number, gen: number, cache?: Expr[][]}}
|
|
33
|
+
*/
|
|
34
|
+
function search (seed, options, predicate) {
|
|
35
|
+
const {
|
|
36
|
+
depth = 16,
|
|
37
|
+
infer = true,
|
|
38
|
+
progressInterval = 1000,
|
|
39
|
+
} = options;
|
|
40
|
+
const hasSeen = infer && !options.noskip;
|
|
41
|
+
|
|
42
|
+
// cache[i] = ith generation, 0 is empty
|
|
43
|
+
const cache = [[]];
|
|
44
|
+
let total = 0;
|
|
45
|
+
let probed = 0;
|
|
46
|
+
const seen = {};
|
|
47
|
+
|
|
48
|
+
const maybeProbe = term => {
|
|
49
|
+
total++;
|
|
50
|
+
const props = infer ? term.infer({ max: options.max, maxArgs: options.maxArgs }) : null;
|
|
51
|
+
if (hasSeen && props.expr) {
|
|
52
|
+
if (seen[props.expr])
|
|
53
|
+
return { res: -1 };
|
|
54
|
+
seen[props.expr] = true;
|
|
55
|
+
}
|
|
56
|
+
probed++;
|
|
57
|
+
const res = predicate(term, props);
|
|
58
|
+
return { res, props };
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// sieve through the seed
|
|
62
|
+
for (const term of seed) {
|
|
63
|
+
const { res } = maybeProbe(term);
|
|
64
|
+
if (res > 0)
|
|
65
|
+
return { expr: term, total, probed, gen: 1 };
|
|
66
|
+
else if (res < 0)
|
|
67
|
+
continue;
|
|
68
|
+
|
|
69
|
+
cache[0].push(term);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let lastProgress;
|
|
73
|
+
|
|
74
|
+
for (let gen = 1; gen < depth; gen++) {
|
|
75
|
+
if (options.progress) {
|
|
76
|
+
options.progress({ gen, total, probed, step: true });
|
|
77
|
+
lastProgress = total;
|
|
78
|
+
}
|
|
79
|
+
for (let i = 0; i < gen; i++) {
|
|
80
|
+
for (const a of cache[gen - i - 1] || []) {
|
|
81
|
+
for (const b of cache[i] || []) {
|
|
82
|
+
if (total >= options.tries)
|
|
83
|
+
return { total, probed, gen, ...(options.retain ? { cache } : {}) };
|
|
84
|
+
if (options.progress && total - lastProgress >= progressInterval) {
|
|
85
|
+
options.progress({ gen, total, probed, step: false });
|
|
86
|
+
lastProgress = total;
|
|
87
|
+
}
|
|
88
|
+
const term = a.apply(b);
|
|
89
|
+
const { res, props } = maybeProbe(term);
|
|
90
|
+
|
|
91
|
+
if (res > 0)
|
|
92
|
+
return { expr: term, total, probed, gen, ...(options.retain ? { cache } : {}) };
|
|
93
|
+
else if (res < 0)
|
|
94
|
+
continue;
|
|
95
|
+
|
|
96
|
+
// if the term is not reducible, it is more likely to be a dead end, so we push it further away
|
|
97
|
+
const offset = infer
|
|
98
|
+
? ((props.expr ? 0 : 3) + (props.dup ? 1 : 0) + (props.proper ? 0 : 1))
|
|
99
|
+
: 0;
|
|
100
|
+
if (!cache[gen + offset])
|
|
101
|
+
cache[gen + offset] = [];
|
|
102
|
+
cache[gen + offset].push(term);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return { total, probed, gen: depth, ...(options.retain ? { cache } : {}) };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* @desc Recursively replace all instances of Expr in a data structure with
|
|
113
|
+
* respective string representation using the format() options.
|
|
114
|
+
* Objects of other types and primitive values are eft as is.
|
|
115
|
+
*
|
|
116
|
+
* May be useful for debugging or diagnostic output.
|
|
117
|
+
*
|
|
118
|
+
* @experimental
|
|
119
|
+
*
|
|
120
|
+
* @param {any} obj
|
|
121
|
+
* @param {object} [options] - see Expr.format()
|
|
122
|
+
* @returns {any}
|
|
123
|
+
*/
|
|
124
|
+
function deepFormat (obj, options = {}) {
|
|
125
|
+
if (obj instanceof Expr)
|
|
126
|
+
return obj.format(options);
|
|
127
|
+
if (Array.isArray(obj))
|
|
128
|
+
return obj.map(deepFormat);
|
|
129
|
+
if (typeof obj !== 'object' || obj === null || obj.constructor !== Object)
|
|
130
|
+
return obj;
|
|
131
|
+
|
|
132
|
+
// default = plain object
|
|
133
|
+
const out = {};
|
|
134
|
+
for (const key in obj)
|
|
135
|
+
out[key] = deepFormat(obj[key]);
|
|
136
|
+
|
|
137
|
+
return out;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
module.exports = { search, deepFormat };
|
package/lib/internal.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
class Tokenizer {
|
|
2
|
+
/**
|
|
3
|
+
* @desc Create a tokenizer that splits strings into tokens according to the given terms.
|
|
4
|
+
* The terms are interpreted as regular expressions, and are sorted by length
|
|
5
|
+
* to ensure that longer matches are preferred over shorter ones.
|
|
6
|
+
* @param {...string|RegExp} terms
|
|
7
|
+
*/
|
|
8
|
+
constructor (...terms) {
|
|
9
|
+
const src = '$|(\\s+)|' + terms
|
|
10
|
+
.map(s => '(?:' + s + ')')
|
|
11
|
+
.sort((a, b) => b.length - a.length)
|
|
12
|
+
.join('|');
|
|
13
|
+
this.rex = new RegExp(src, 'gys');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @desc Split the given string into tokens according to the terms specified in the constructor.
|
|
18
|
+
* @param {string} str
|
|
19
|
+
* @return {string[]}
|
|
20
|
+
*/
|
|
21
|
+
split (str) {
|
|
22
|
+
this.rex.lastIndex = 0;
|
|
23
|
+
const list = [...str.matchAll(this.rex)];
|
|
24
|
+
|
|
25
|
+
// did we parse everything?
|
|
26
|
+
const eol = list.pop();
|
|
27
|
+
const last = eol?.index ?? 0;
|
|
28
|
+
|
|
29
|
+
if (last !== str.length) {
|
|
30
|
+
throw new Error('Unknown tokens at pos ' + last + '/' + str.length
|
|
31
|
+
+ ' starting with ' + str.substring(last));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// skip whitespace
|
|
35
|
+
return list.filter(x => x[1] === undefined).map(x => x[0]);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const tokRestrict = new Tokenizer('[-=+]', '[A-Z]', '\\b[a-z_][a-z_0-9]*\\b');
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @desc Add ot remove tokens from a set according to a spec string.
|
|
43
|
+
* The spec string is a sequence of tokens, with each group optionally prefixed
|
|
44
|
+
* by one of the operators '=', '+', or '-'.
|
|
45
|
+
* The '=' operator resets the set to contain only the following token(s).
|
|
46
|
+
* @param {Set<string>} set
|
|
47
|
+
* @param {string} [spec]
|
|
48
|
+
* @returns {Set<string>}
|
|
49
|
+
*/
|
|
50
|
+
function restrict (set, spec) {
|
|
51
|
+
if (!spec)
|
|
52
|
+
return set;
|
|
53
|
+
let out = new Set([...set]);
|
|
54
|
+
const act = {
|
|
55
|
+
'=': sym => { out = new Set([sym]); mode = '+'; },
|
|
56
|
+
'+': sym => { out.add(sym); },
|
|
57
|
+
'-': sym => { out.delete(sym); },
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
let mode = '=';
|
|
61
|
+
for (const sym of tokRestrict.split(spec)) {
|
|
62
|
+
if (act[sym])
|
|
63
|
+
mode = sym;
|
|
64
|
+
else
|
|
65
|
+
act[mode](sym);
|
|
66
|
+
}
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
class ActionWrapper {
|
|
71
|
+
/**
|
|
72
|
+
* @template T
|
|
73
|
+
* @param {T} value
|
|
74
|
+
* @param {string} action
|
|
75
|
+
*/
|
|
76
|
+
constructor (value, action) {
|
|
77
|
+
this.value = value;
|
|
78
|
+
this.action = action;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* @private
|
|
84
|
+
* @template T
|
|
85
|
+
* @param {T|ActionWrapper<T>} value
|
|
86
|
+
* @returns {[T?, string|undefined]}
|
|
87
|
+
*/
|
|
88
|
+
function unwrap (value) {
|
|
89
|
+
if (value instanceof ActionWrapper)
|
|
90
|
+
return [value.value ?? undefined, value.action];
|
|
91
|
+
return [value ?? undefined, undefined];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
*
|
|
96
|
+
* @private
|
|
97
|
+
* @template T
|
|
98
|
+
* @param {string} action
|
|
99
|
+
* @returns {function(T): ActionWrapper<T>}
|
|
100
|
+
*/
|
|
101
|
+
function prepareWrapper (action) {
|
|
102
|
+
return value => new ActionWrapper(value, action);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
module.exports = { Tokenizer, restrict, unwrap, prepareWrapper };
|