@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/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 generic combinatory logic expression.
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
- return args.length > 0 ? new App(this, ...args) : this;
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 rought estimate of the complexity of the term
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: number?, maxArgs: number?}} options
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._firstVar())
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
- _aslist () {
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 = naiveCanonize(this, options);
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: number?}} options
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, ...args) directly, use fun.apply(...args) instead.
574
+ * Never ever use new App(fun, arg) directly, use fun.apply(...args) instead.
478
575
  * @param {Expr} fun
479
- * @param {Expr} args
576
+ * @param {Expr} arg
480
577
  */
481
- constructor (fun, ...args) {
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 = args.pop();
487
- this.fun = args.length ? new App(fun, ...args) : 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._aslist();
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, new App(first, ...out), {
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
- split () {
626
- // leftover from array-based older design
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 [fst, snd] = impl.split();
1032
+ const { fun, arg } = impl;
924
1033
  // try eta reduction
925
- if (snd === this.arg && !fst.any(e => e === this.arg))
926
- return fst._rski(options);
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, fst))._rski(options),
930
- (new Lambda(this.arg, snd))._rski(options)
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 [fun, arg] = expr.split();
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 };
@@ -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 };