@dallaylaen/ski-interpreter 1.0.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/parser.js ADDED
@@ -0,0 +1,326 @@
1
+ /**
2
+ * Combinatory logic simulator
3
+ */
4
+
5
+ const { Tokenizer, restrict } = require('./util');
6
+ const { globalOptions, Expr, App, FreeVar, Lambda, Native, Alias, Church, native } = require('./expr');
7
+
8
+ class Empty extends Expr {
9
+ apply (...args) {
10
+ return args.length ? args.shift().apply(...args) : this;
11
+ }
12
+
13
+ postParse () {
14
+ throw new Error('Attempt to use empty expression () as a term');
15
+ }
16
+ }
17
+
18
+ class PartialLambda extends Empty {
19
+ // TODO mutable! rewrite ro when have time
20
+ constructor (term, known = {}) {
21
+ super();
22
+ this.impl = new Empty();
23
+ if (term instanceof FreeVar)
24
+ this.terms = [term];
25
+ else if (term instanceof PartialLambda) {
26
+ if (!(term.impl instanceof FreeVar))
27
+ throw new Error('Expected FreeVar->...->FreeVar->Expr');
28
+ this.terms = [...term.terms, term.impl];
29
+ } else
30
+ throw new Error('Expected FreeVar or PartialLambda');
31
+ }
32
+
33
+ apply (term, ...tail) {
34
+ if (term === null || tail.length !== 0 )
35
+ throw new Error('bad syntax in partial lambda expr');
36
+ this.impl = this.impl.apply(term);
37
+ return this;
38
+ }
39
+
40
+ postParse () {
41
+ return new Lambda(this.terms, this.impl);
42
+ }
43
+
44
+ // uncomment if debugging with prints
45
+ /* toString () {
46
+ return this.terms.join('->') + '->' + (this.impl ?? '???');
47
+ } */
48
+ }
49
+
50
+ const combChars = new Tokenizer(
51
+ '[()]', '[A-Z]', '[a-z_][a-z_0-9]*', '\\b[0-9]+\\b', '->', '\\+'
52
+ );
53
+
54
+ class SKI {
55
+ /**
56
+ *
57
+ * @param {{
58
+ * allow: string?,
59
+ * numbers: boolean?,
60
+ * lambdas: boolean?,
61
+ * terms: { [key: string]: Expr|string}?,
62
+ * annotate: boolean?,
63
+ * }} [options]
64
+ */
65
+ constructor (options = {}) {
66
+ this.annotate = options.annotate ?? false;
67
+ this.known = { ...native };
68
+ this.hasNumbers = true;
69
+ this.hasLambdas = true;
70
+ this.allow = new Set(Object.keys(this.known));
71
+
72
+ // Import terms, if any. Omit native ones
73
+ for (const name in options.terms ?? {}) {
74
+ // Native terms already handled by allow
75
+ if (!options.terms[name].match(/^Native:/))
76
+ this.add(name, options.terms[name]);
77
+ }
78
+
79
+ // Finally, impose restrictions
80
+ // We must do it after recreating terms, or else terms reliant on forbidden terms will fail
81
+ this.hasNumbers = options.numbers ?? true;
82
+ this.hasLambdas = options.lambdas ?? true;
83
+ if (options.allow)
84
+ this.restrict(options.allow);
85
+ }
86
+
87
+ /**
88
+ *
89
+ * @param {Alias|String} term
90
+ * @param {Expr|String|[number, function(...Expr): Expr, {note: string?, fast: boolean?}]} [impl]
91
+ * @param {String} [note]
92
+ * @return {SKI} chainable
93
+ */
94
+ add (term, impl, note ) {
95
+ if (typeof term === 'string') {
96
+ if (typeof impl === 'string')
97
+ term = new Alias(term, this.parse(impl), { canonize: true });
98
+ else if (impl instanceof Expr)
99
+ term = new Alias(term, impl, { canonize: true });
100
+ else
101
+ throw new Error('add: term must be an Alias or a string and impl must be an Expr or a string');
102
+ } else if (term instanceof Alias)
103
+ term = new Alias(term.name, term.impl, { canonize: true });
104
+
105
+ // This should normally be unreachable but let's keep just in case
106
+ if (!(term instanceof Alias))
107
+ throw new Error('add: term must be an Alias or a string (accompanied with an implementation)');
108
+
109
+ if (this.annotate && note === undefined && term.canonical)
110
+ note = term.canonical.toString({ terse: true, html: true });
111
+ if (note !== undefined)
112
+ term.note = note;
113
+
114
+ this.known['' + term] = term;
115
+ this.allow.add('' + term);
116
+
117
+ return this;
118
+ }
119
+
120
+ maybeAdd (name, impl) {
121
+ if (this.known[name])
122
+ this.allow.add(name);
123
+ else
124
+ this.add(name, impl);
125
+ return this;
126
+ }
127
+
128
+ /**
129
+ * Restrict the interpreter to given terms. Terms prepended with '+' will be added
130
+ * and terms preceeded with '-' will be removed.
131
+ * @example ski.restrict('SK') // use the basis
132
+ * @example ski.restrict('+I') // allow I now
133
+ * @example ski.restrict('-SKI +BCKW' ); // switch basis
134
+ * @example ski.restrict('-foo -bar'); // forbid some user functions
135
+ * @param {string} spec
136
+ * @return {SKI} chainable
137
+ */
138
+ restrict (spec) {
139
+ this.allow = restrict(this.allow, spec);
140
+ return this;
141
+ }
142
+
143
+ /**
144
+ *
145
+ * @param {string} spec
146
+ * @return {string}
147
+ */
148
+ showRestrict (spec = '+') {
149
+ const out = [];
150
+ let prevShort = true;
151
+ for (const term of [...restrict(this.allow, spec)].sort()) {
152
+ const nextShort = term.match(/^[A-Z]$/);
153
+ if (out.length && !(prevShort && nextShort))
154
+ out.push(' ');
155
+ out.push(term);
156
+ prevShort = nextShort;
157
+ }
158
+ return out.join('');
159
+ }
160
+
161
+ /**
162
+ *
163
+ * @param {String} name
164
+ * @return {SKI}
165
+ */
166
+ remove (name) {
167
+ this.known[name].outdated = true;
168
+ delete this.known[name];
169
+ this.allow.delete(name);
170
+ return this;
171
+ }
172
+
173
+ /**
174
+ *
175
+ * @return {{[key:string]: Native|Alias}}
176
+ */
177
+ getTerms () {
178
+ const out = {};
179
+ for (const name of Object.keys(this.known)) {
180
+ if (this.allow.has(name))
181
+ out[name] = this.known[name];
182
+ }
183
+ return out;
184
+ }
185
+
186
+ /**
187
+ *
188
+ * @param {string} source
189
+ * @param {{[keys: string]: Expr}} vars
190
+ * @param {{numbers: boolean?, lambdas: boolean?, allow: string?}} options
191
+ * @return {Expr}
192
+ */
193
+ parse (source, vars = {}, options = {}) {
194
+ const lines = source.replace(/\/\/[^\n]*$/gm, '')
195
+ .split(/\s*;[\s;]*/).filter( s => s.match(/\S/));
196
+
197
+ const jar = { ...vars };
198
+
199
+ let expr = new Empty();
200
+ for (const item of lines) {
201
+ const [_, save, str] = item.match(/^(?:\s*([A-Z]|[a-z][a-z_0-9]*)\s*=\s*)?(.*)$/s);
202
+ if (expr instanceof Alias)
203
+ expr.outdated = true;
204
+ expr = this.parseLine(str, jar, options);
205
+
206
+ if (save !== undefined) {
207
+ if (jar[save] !== undefined)
208
+ throw new Error('Attempt to redefine a known term: ' + save);
209
+ expr = new Alias(save, expr);
210
+ jar[save] = expr;
211
+ }
212
+
213
+ // console.log('parsed line:', item, '; got:', expr,'; jar now: ', jar);
214
+ }
215
+
216
+ // reimport free variables, so that co-parsing x(y(z)) and z(x(y)) with the same jar
217
+ // results in _equal_ free vars and not just ones with the same name
218
+ for (const name in jar) {
219
+ if (!vars[name] && jar[name] instanceof SKI.classes.FreeVar)
220
+ vars[name] = jar[name];
221
+ }
222
+
223
+ return expr;
224
+ }
225
+
226
+ /**
227
+ *
228
+ * @param {String} source S(KI)I
229
+ * @param {{[keys: string]: Expr}} vars
230
+ * @param {{numbers: boolean?, lambdas: boolean?, allow: string?}} options
231
+ * @return {Expr} parsed expression
232
+ */
233
+ parseLine (source, vars = {}, options = {}) {
234
+ const opt = {
235
+ numbers: options.numbers ?? this.hasNumbers,
236
+ lambdas: options.lambdas ?? this.hasLambdas,
237
+ allow: restrict(this.allow, options.allow),
238
+ };
239
+ // make sure '+' usage is in sync with numerals
240
+ opt.numbers ? opt.allow.add('+') : opt.allow.delete('+');
241
+
242
+ const tokens = combChars.split(source);
243
+
244
+ const empty = new Empty();
245
+ /** @type {Expr[]} */
246
+ const stack = [empty];
247
+
248
+ // TODO each token should carry along its position in source
249
+ for (const c of tokens) {
250
+ // console.log("parseLine: found "+c+"; stack =", stack.join(", "));
251
+ if (c === '(')
252
+ stack.push(empty);
253
+ else if (c === ')') {
254
+ if (stack.length < 2)
255
+ throw new Error('unbalanced input: extra closing parenthesis' + source);
256
+ const x = stack.pop().postParse();
257
+ const f = stack.pop();
258
+ stack.push(f.apply(x));
259
+ } else if (c === '->') {
260
+ if (!opt.lambdas)
261
+ throw new Error('Lambdas not supported, allow them explicitly');
262
+ stack.push(new PartialLambda(stack.pop(), vars));
263
+ } else if (c.match(/^[0-9]+$/)) {
264
+ if (!opt.numbers)
265
+ throw new Error('Church numbers not supported, allow them explicitly');
266
+ const f = stack.pop();
267
+ stack.push(f.apply(new Church(c)));
268
+ } else {
269
+ const f = stack.pop();
270
+ if (!vars[c] && this.known[c] && !opt.allow.has(c)) {
271
+ throw new Error('Term \'' + c + '\' is not in the restricted set '
272
+ + [...opt.allow].sort().join(' '));
273
+ }
274
+ // look in temp vars first, then in known terms, then fallback to creating free var
275
+ const x = vars[c] ?? this.known[c] ?? (vars[c] = new FreeVar(c));
276
+ stack.push(f.apply(x));
277
+ }
278
+ }
279
+
280
+ if (stack.length !== 1) {
281
+ throw new Error('unbalanced input: missing '
282
+ + (stack.length - 1) + ' closing parenthesis:' + source);
283
+ }
284
+
285
+ return stack.pop().postParse();
286
+ }
287
+
288
+ toJSON () {
289
+ return {
290
+ allow: this.showRestrict('+'),
291
+ numbers: this.hasNumbers,
292
+ lambdas: this.hasLambdas,
293
+ terms: this.getTerms(),
294
+ annotate: this.annotate,
295
+ }
296
+ }
297
+ }
298
+
299
+ // Create shortcuts for common terms
300
+ /**
301
+ * Create free var(s) for subsequent use
302
+ * @param {String} names
303
+ * @return {FreeVar[]}
304
+ */
305
+ SKI.free = (...names) => names.map(s => new FreeVar(s));
306
+
307
+ /**
308
+ * Convert a number to Church encoding
309
+ * @param {number} n
310
+ * @return {Church}
311
+ */
312
+ SKI.church = n => new Church(n);
313
+ SKI.classes = { Expr, Native, Alias, FreeVar, Lambda, Church };
314
+
315
+ /**
316
+ *
317
+ * @type {{[key: string]: Native}}
318
+ */
319
+
320
+ for (const name in native)
321
+ SKI[name] = native[name];
322
+ SKI.native = native;
323
+ SKI.options = globalOptions;
324
+ SKI.lambdaPlaceholder = Expr.lambdaPlaceholder;
325
+
326
+ module.exports = { SKI };
package/lib/quest.js ADDED
@@ -0,0 +1,311 @@
1
+ const { SKI } = require('./parser');
2
+ const { Expr, FreeVar, Alias, Lambda } = SKI.classes;
3
+
4
+ /**
5
+ * @typedef {{
6
+ * pass: boolean,
7
+ * reason: string?,
8
+ * steps: number,
9
+ * start: Expr,
10
+ * found: Expr,
11
+ * expected: Expr,
12
+ * note: string?,
13
+ * args: Expr[],
14
+ * case: Case
15
+ * }} CaseResult
16
+ */
17
+
18
+ class Quest {
19
+ /**
20
+ * @description A combinator problem with a set of test cases for the proposed solution.
21
+ * @param {{
22
+ * title: string?,
23
+ * descr: string?,
24
+ * subst: string?,
25
+ * allow: string?,
26
+ * numbers: boolean?,
27
+ * vars: string[]?,
28
+ * engine: SKI?,
29
+ * engineFull: SKI?,
30
+ * cases: [{max: number?, note: string?, feedInput: boolean, lambdas: boolean?}|string[], ...string[][]]?
31
+ * }} options
32
+ */
33
+ constructor (options = {}) {
34
+ const { input, vars, cases, allow, numbers, lambdas, subst, engine, engineFull, ...meta } = options;
35
+
36
+ //
37
+ this.engine = engine ?? new SKI();
38
+ this.engineFull = engineFull ?? new SKI();
39
+ this.restrict = { allow, numbers: numbers ?? false, lambdas: lambdas ?? false };
40
+ this.vars = {};
41
+ this.subst = Array.isArray(subst) ? subst : [subst ?? 'phi'];
42
+
43
+ const jar = {};
44
+
45
+ // options.vars is a list of expressions.
46
+ // we suck all free variables + all term declarations from there into this.vars
47
+ // to feed it later to every case's parser.
48
+ for (const term of vars ?? []) {
49
+ const expr = this.engineFull.parse(term, jar);
50
+ if (expr instanceof SKI.classes.Alias)
51
+ this.vars[expr.name] = new Alias(expr.name, expr.impl, { terminal: true, canonize: false });
52
+ // Canonized aliases won't expand with insufficient arguments,
53
+ // causing correct solutions to fail, so alas...
54
+ else if (expr instanceof SKI.classes.FreeVar)
55
+ this.vars[expr.name] = expr;
56
+ else
57
+ throw new Error('Unsupported given variable type: ' + term);
58
+ }
59
+
60
+ this.input = [];
61
+ for (const term of Array.isArray(input) ? input : [input])
62
+ this.addInput(term);
63
+ if (!this.input.length)
64
+ throw new Error('Quest needs at least one input placeholder');
65
+ if (subst)
66
+ this.input[0].fancy = this.subst[0];
67
+
68
+ this.varsFull = { ...this.vars, ...jar };
69
+ for (const term of this.input) {
70
+ if (term.name in this.varsFull)
71
+ throw new Error('input placeholder name is duplicated or clashes with vars: ' + term.name);
72
+ this.varsFull[term.name] = term.placeholder;
73
+ }
74
+
75
+ this.cases = [];
76
+ this.title = meta.title;
77
+ meta.descr = list2str(meta.descr);
78
+ this.descr = meta.descr;
79
+ this.meta = meta;
80
+
81
+ for (const c of cases ?? [])
82
+ this.add(...c);
83
+ }
84
+
85
+ /**
86
+ * Display allowed terms based on what engine thinks of this.vars + this.restrict.allow
87
+ * @return {string}
88
+ */
89
+ allowed () {
90
+ const allow = this.restrict.allow ?? '';
91
+ const vars = Object.keys(this.vars).sort();
92
+ // In case vars are present and restrictions aren't, don't clutter the output with all the known terms
93
+ return allow
94
+ ? this.engine.showRestrict(allow + '+' + vars.join(' '))
95
+ : vars.map( s => '+' + s).join(' ');
96
+ }
97
+
98
+ addInput (term) {
99
+ if (typeof term !== 'object')
100
+ term = { name: term };
101
+ if (typeof term.name !== 'string')
102
+ throw new Error("quest 'input' field must be a string or a {name: string, ...} object");
103
+
104
+ [term.placeholder] = SKI.free(term.name);
105
+ // TODO more checks
106
+ this.input.push(term);
107
+ }
108
+
109
+ /**
110
+ *
111
+ * @param {{} | string} opt
112
+ * @param {string} terms
113
+ * @return {Quest}
114
+ */
115
+ add (opt, ...terms) {
116
+ if (typeof opt === 'string') {
117
+ terms.unshift(opt);
118
+ opt = {};
119
+ } else
120
+ opt = { ...opt };
121
+
122
+ opt.engine = opt.engine ?? this.engineFull;
123
+ opt.vars = opt.vars ?? this.varsFull;
124
+
125
+ const input = this.input.map( t => t.placeholder );
126
+ this.cases.push(
127
+ opt.linear
128
+ ? new LinearCase(input, opt, terms)
129
+ : new ExprCase(input, opt, terms)
130
+ );
131
+ return this;
132
+ }
133
+
134
+ /**
135
+ * @description Statefully parse a list of strings into expressions or fancy aliases thereof.
136
+ * @param {string[]} input
137
+ * @return {{terms: Expr[], weight: number}}
138
+ */
139
+ prepare (...input) {
140
+ if (input.length !== this.input.length)
141
+ throw new Error('Solutions provided ' + input.length + ' terms where ' + this.input.length + ' are expected');
142
+
143
+ let weight = 0;
144
+ const prepared = [];
145
+ const jar = { ...this.vars };
146
+ for (let i = 0; i < input.length; i++) {
147
+ const spec = this.input[i];
148
+ const impl = this.engine.parse(input[i], jar, {
149
+ allow: spec.allow ?? this.restrict.allow,
150
+ numbers: spec.numbers ?? this.restrict.numbers,
151
+ lambdas: spec.lambdas ?? this.restrict.lambdas,
152
+ });
153
+ weight += impl.weight();
154
+ const expr = impl instanceof FreeVar
155
+ ? impl
156
+ : new Alias(spec.fancy ?? spec.name, impl, { terminal: true, canonize: false });
157
+ jar[spec.name] = expr;
158
+ prepared.push(expr);
159
+ }
160
+ return {
161
+ prepared,
162
+ weight,
163
+ };
164
+ }
165
+
166
+ /**
167
+ *
168
+ * @param {string} input
169
+ * @return {{
170
+ * expr: Expr?,
171
+ * pass: boolean,
172
+ * details: CaseResult[],
173
+ * exception: Error?,
174
+ * steps: number,
175
+ * input: Expr[]|string[],
176
+ * weight: number?
177
+ * }}
178
+ */
179
+ check (...input) {
180
+ try {
181
+ const { prepared, weight } = this.prepare(...input);
182
+ const details = this.cases.map( c => c.check(...prepared) );
183
+ const pass = details.reduce((acc, val) => acc && val.pass, true);
184
+ const steps = details.reduce((acc, val) => acc + val.steps, 0);
185
+ return {
186
+ expr: prepared[0],
187
+ input: prepared,
188
+ pass,
189
+ steps,
190
+ details,
191
+ weight,
192
+ };
193
+ } catch (e) {
194
+ return { pass: false, details: [], exception: e, steps: 0, input };
195
+ }
196
+ }
197
+
198
+ /**
199
+ *
200
+ * @return {TestCase[]}
201
+ */
202
+ show () {
203
+ return [...this.cases];
204
+ }
205
+ }
206
+
207
+ class Case {
208
+ constructor (input, options) {
209
+ this.max = options.max ?? 1000;
210
+ this.note = options.note;
211
+ this.vars = { ...(options.vars ?? {}) }; // shallow copy to avoid modifying the original
212
+ this.input = input;
213
+ this.engine = options.engine;
214
+ }
215
+
216
+ parse (src) {
217
+ return new Lambda(this.input, this.engine.parse(src, this.vars));
218
+ }
219
+
220
+ /**
221
+ * @param {Expr} expr
222
+ * @return {CaseResult}
223
+ */
224
+ check ( ...expr ) {
225
+ throw new Error('not implemented');
226
+ }
227
+ }
228
+
229
+ class ExprCase extends Case {
230
+ /**
231
+ * @param {FreeVar[]} input
232
+ * @param {{
233
+ * max: number?,
234
+ * note: string?,
235
+ * vars: {string: Expr}?,
236
+ * engine: SKI?
237
+ * }} options
238
+ * @param {[e1: string, e2: string]} terms
239
+ */
240
+ constructor (input, options, terms) {
241
+ if (terms.length !== 2)
242
+ throw new Error('Case accepts exactly 2 strings');
243
+
244
+ super(input, options);
245
+
246
+ [this.e1, this.e2] = terms.map(src => this.parse(src));
247
+ }
248
+
249
+ check (...expr) {
250
+ // we do it the fancy way and instead of just "apply" to avoid
251
+ // displaying (foo->foo this that)(user input) as 1st step
252
+ const subst = (outer, inner) => outer.reduce(inner) ?? outer.apply(...inner);
253
+
254
+ const start = subst(this.e1, expr);
255
+ const r1 = start.run({ max: this.max });
256
+ const r2 = subst(this.e2, expr).run({ max: this.max });
257
+ let reason = null;
258
+ if (!r1.final || !r2.final)
259
+ reason = 'failed to reach normal form in ' + this.max + ' steps';
260
+ else if (!r1.expr.equals(r2.expr))
261
+ reason = 'expected: ' + r2.expr;
262
+ // NOTE maybe there should be expand() on both sides of equal() but we'll see.
263
+
264
+ return {
265
+ pass: !reason,
266
+ reason,
267
+ steps: r1.steps,
268
+ start,
269
+ found: r1.expr,
270
+ expected: r2.expr,
271
+ note: this.note,
272
+ args: expr,
273
+ case: this,
274
+ };
275
+ }
276
+ }
277
+
278
+ class LinearCase extends Case {
279
+ // test that an expression uses all of its inputs exactly once
280
+ constructor (input, options, terms) {
281
+ super(input, options);
282
+ this.expr = this.parse(terms[0]);
283
+ }
284
+
285
+ check (...expr) {
286
+ const start = this.expr.apply(...expr);
287
+ const r = start.run({ max: this.max });
288
+ const arity = r.expr.canonize();
289
+ const reason = arity.linear
290
+ ? null
291
+ : 'expected a linear expression, i.e. such that uses all inputs exactly once';
292
+ return {
293
+ pass: !reason,
294
+ reason,
295
+ steps: r.steps,
296
+ start,
297
+ found: r.expr,
298
+ case: this,
299
+ note: this.note,
300
+ args: expr,
301
+ }
302
+ }
303
+ }
304
+
305
+ function list2str (str) {
306
+ if (str === undefined)
307
+ return str;
308
+ return Array.isArray(str) ? str.join(' ') : '' + str;
309
+ }
310
+
311
+ module.exports = { Quest };
package/lib/util.js ADDED
@@ -0,0 +1,74 @@
1
+ class Tokenizer {
2
+ constructor (...terms) {
3
+ const src = '$|(\\s+)|' + terms
4
+ .map(s => '(?:' + s + ')')
5
+ .sort((a, b) => b.length - a.length)
6
+ .join('|');
7
+ this.rex = new RegExp(src, 'gys');
8
+ }
9
+
10
+ /**
11
+ *
12
+ * @param {string} str
13
+ * @return {string[]}
14
+ */
15
+ split (str) {
16
+ this.rex.lastIndex = 0;
17
+ const list = [...str.matchAll(this.rex)];
18
+
19
+ // did we parse everything?
20
+ const eol = list.pop();
21
+ const last = eol?.index ?? 0;
22
+
23
+ if (last !== str.length) {
24
+ throw new Error('Unknown tokens at pos ' + last + '/' + str.length
25
+ + ' starting with ' + str.substring(last));
26
+ }
27
+
28
+ // skip whitespace
29
+ return list.filter(x => x[1] === undefined).map(x => x[0]);
30
+ }
31
+ }
32
+
33
+ const tokRestrict = new Tokenizer('[-=+]', '[A-Z]', '\\b[a-z_][a-z_0-9]*\\b');
34
+ function restrict (set, spec) {
35
+ if (!spec)
36
+ return set;
37
+ let out = new Set([...set]);
38
+ let mode = 0;
39
+ const act = [
40
+ sym => { out = new Set([sym]); mode = 1; },
41
+ sym => { out.add(sym); },
42
+ sym => { out.delete(sym); },
43
+ ];
44
+ for (const sym of tokRestrict.split(spec)) {
45
+ if (sym === '=')
46
+ mode = 0;
47
+ else if (sym === '+')
48
+ mode = +1;
49
+ else if (sym === '-')
50
+ mode = 2;
51
+ else
52
+ act[mode](sym);
53
+ }
54
+ return out;
55
+ }
56
+
57
+ function missingIndices (arr, set) {
58
+ const out = new Set();
59
+ for (let n = 0; n < arr.length; n++) {
60
+ if (!set.has(arr[n]))
61
+ out.add(n);
62
+ }
63
+ return out;
64
+ }
65
+
66
+ function isSubset (a, b) {
67
+ for (const x of a) {
68
+ if (!b.has(x))
69
+ return false;
70
+ }
71
+ return true;
72
+ }
73
+
74
+ module.exports = { Tokenizer, restrict, missingIndices, isSubset };